iTranslated by AI
Building a Simple Blog - Part 2 (with Next.js & Contentful)
Introduction
This article is a continuation of the previous one. Please check out the previous article as well!
Goals of this Article
- Introduce
Tailwind CSS - Display different types of articles based on category
- Add dark mode
- Deploy to
Vercel
We aim to achieve these four goals.
↓ Finished Product
Introducing Tailwind CSS
We will introduce Tailwind CSS for styling. The official documentation provides instructions on how to integrate Tailwind CSS with Next.js.
Tailwind CSS is also easy to customize.
module.exports = {
purge: ['./pages/**/*.{tsx}', './components/**/*.{tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
+ colors: {
+ black: '#000',
+ white: '#fff',
+ blue: '#1985A1',
+ purple: '#8257e6',
+ orange: '#fa923f',
+ subtext: '#617482',
+ darkblue: '#06202A'
+ },
+ animation: {
+ none: 'none',
+ spin: 'spin 1s linear infinite',
+ ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
+ pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
+ bounce: 'bounce 1s infinite'
+ }
},
variants: {
extend: {}
},
plugins: []
}
Also, if you install the Color Highlight extension for VS Code, colors can be visualized.
As an example, styling can be done concisely.
import React from 'react'
const index = () => {
return (
<div className="flex w-full mt-5 justify-evenly">
<pc className="w-5 h-5 rounded-full animate-bounce bg-orange" />
<pc className="w-5 h-5 rounded-t-sm animate-spin bg-blue" />
<pc className="w-5 h-5 rounded-full animate-ping bg-purple" />
<pc className="w-5 h-5 rounded-full animate-pulse bg-subtext" />
<pc className="w-5 h-5 rounded-full animate-none bg-darkblue" />
</div>
)
}
export default index
Writing Code
We will now prepare to achieve the remaining three goals mentioned earlier.
First, let's style with Tailwind CSS.
import React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<title>Next Blog</title>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Kaushan+Script&display=swap"
rel="stylesheet"
/>
</Head>
<body className="bg-fixed bg-gradient-to-r from-orange to-purple">
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
import React, { useState } from 'react'
import Head from 'next/head'
import { createClient, EntryCollection } from 'contentful'
import { InferGetStaticPropsType, NextPage } from 'next'
import BlogCard from '../components/BlogCard'
import Sidebar from '../components/BlogNavbar'
import { Category, IFields } from '../../types'
export const getStaticProps = async () => {
const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_KEY
})
const response: EntryCollection<IFields> = await client.getEntries({
content_type: 'article'
})
return {
props: {
article: response.items
}
}
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const Home: NextPage<Props> = ({ article }) => {
return (
<div className="px-5 py-2 overflow-scroll">
<div className="grid grid-cols-12 gap-6 px-5 my-14 lg:mb-0 md:mb-16 sm:px-20 md:px-32 lg:px-36 xl:px-48 ">
<div className="h-full col-span-12 p-4 text-base text-center bg-white lg:col-span-3 rounded-2xl shadow-custom-light">
<Sidebar/>
</div>
<div className="flex flex-col col-span-12 overflow-hidden bg-white shadow-custom-light rounded-2xl lg:col-span-9">
<div className="relative grid grid-cols-12 gap-4 my-3">
{article.map(item => (
<div
key={item.sys.id}
className="col-span-12 p-2 bg-gray-200 rounded-lg sm:col-span-6 lg:col-span-4"
>
<BlogCard item={item} />
</div>
))}
</div>
</div>
</div>
</div>
)
}
export default Home
We connect Next.js and Contentful using SSG. Please refer to the previous article for details. However, the previous article had an issue with props type inference, resulting in any type, which has been corrected. (The previous article has also been updated.)
The appearance is as follows:
From now on, the left side will be referred to as the Sidebar component and the right side as the BlogCard component.
BlogCard Component
import React from 'react'
import { IFields } from '../../types'
import Image from 'next/image'
import { NextPage } from 'next'
import { Entry } from 'contentful'
const BlogCard: NextPage<{ item: Entry<IFields> }> = ({ item }) => {
return (
<>
<Image
src={'https:' + item.fields.thumbnail.fields.file.url}
alt={item.fields.title}
className="object-contain cursor-pointer"
layout="responsive"
height="300"
width="400"
/>
<pc className="my-2 text-center">{item.fields.title}</p>
</>
)
}
export default BlogCard
We use Next.js's Image tag for images. When loading external files (Contentful) like this, you need to create a next.config.js file and add the following:
module.exports = {
images: {
domains: ['images.ctfassets.net'] // ←Changes depending on the external file
}
}
Also, we will add a feature where clicking on each article's image magnifies the image, and clicking the detail (Link tag) leads to the respective article's link.
import React, { useState } from 'react'
import { IFields } from '../../types'
+import { XIcon } from '@heroicons/react/solid'
import Image from 'next/image'
import Link from 'next/link'
import { NextPage } from 'next'
import { Entry } from 'contentful'
const BlogCard: NextPage<{ item: Entry<IFields> }> = ({ item }) => {
+ const [showDetail, setShowDetail] = useState(false)
return (
<>
<Image
src={'https:' + item.fields.thumbnail.fields.file.url}
alt={item.fields.title}
className="object-contain cursor-pointer"
onClick={() => setShowDetail(true)}
layout="responsive"
height="300"
width="400"
/>
<pc className="my-2 text-center">{item.fields.title}</p>
+ {showDetail && (
+ <div className="absolute top-0 left-0 z-10 grid w-full h-full p-2 text-black bg-white md:grid-cols-2 gap-x-12">
+ <Image
+ className="object-contain"
+ src={'https:' + item.fields.thumbnail.fields.file.url}
+ alt={item.fields.title}
+ layout="responsive"
+ height="150"
+ width="300"
+ />
+ <div className="flex justify-center my-4 space-x-3">
+ <Link href={'/article/' + item.fields.slug}>
+ <pa className="flex items-center px-4 space-x-3 text-2xl +font-bold bg-gray-200 hover:text-orange">
+ detail
+ </pa>
+ </Link>
+ <div className="flex items-center text-sm">
+ {item.fields.type.map(tech => (
+ <span
+ key={tech}
+ className="px-2 py-1 my-1 text-subtext"
+ >
+ [{tech}]
+ </span>
+ ))}
+ </div>
+ </div>
+ <button
+ onClick={() => setShowDetail(false)}
+ className="absolute p-1 bg-gray-200 rounded-full top-3 right-3 focus:outline-none"
+ >
+ <XIc on className="h-8" />
+ </button>
+ </div>
+ )}
</>
)
}
export default BlogCard
We are using heroicons for icons.
By the way, item.fields.type (e.g., [dog], [husky] in the image) is one of the Content models created in Contentful. We will use this in the next chapter.
Displaying Articles Based on Article Type
With the preparations complete, we will now move on to the second goal.
First, we will modify the index component to facilitate cooperation between the Sidebar component and the BlogCard component.
const Home: NextPage<Props> = ({ article }) => {
+ const [articles, setArticles] = useState(article)
+ const [active, setActive] = useState('dog')
+ const handlerFilterCategory = (category: Category) => {
+ if (category === 'dog') {
+ setArticles(article)
+ setActive(category)
+ return
+ }
+ const newArray = article.filter(item => item.fields.type.includes(category))
+ setArticles(newArray)
+ setActive(category)
+ }
return (
<div className="px-5 py-2 overflow-scroll">
<div className="grid grid-cols-12 gap-6 px-5 my-14 lg:mb-0 md:mb-16 sm:px-20 md:px-32 lg:px-36 xl:px-48 ">
{/* // do this div style later (after putting the content) */}
<div className="h-full col-span-12 p-4 text-base text-center bg-white lg:col-span-3 rounded-2xl shadow-custom-light">
<Sidebar
+ handlerFilterCategory={handlerFilterCategory}
+ active={active}
/>
</div>
<div className="flex flex-col col-span-12 overflow-hidden bg-white shadow-custom-light rounded-2xl lg:col-span-9">
<div className="relative grid grid-cols-12 gap-4 my-3">
- {article.map(item => (
+ {articles.map(item => (
<div
key={item.sys.id}
className="col-span-12 p-2 bg-gray-200 rounded-lg sm:col-span-6 lg:col-span-4 "
>
<BlogCard item={item} />
</div>
))}
</div>
</div>
</div>
</div>
)
}
export default Home
Key Points
First, prepare two useState hooks.
The first useState takes the data (article) obtained from contentful as an argument and updates the content of the article based on conditions. The handlerFilterCategory function handles this, determining whether all of the article's content or only a part of it is needed based on the argument (category).
If only a part is needed, an array operation is performed using the filter method. If the type of each article created in Contentful includes the category argument of the handlerFilterCategory function, the article's content is updated. This is set as newArray, and state management is performed by setState(setArticles).
The latest state value (articles) is then passed to the BlogCard component via props.
The second useState is prepared for styling with Tailwind CSS. It will be introduced in the following Sidebar component code.
import React from 'react'
import { Category } from '../../types'
export const NavItem: React.FC<{
value: Category
handlerFilterCategory: (category: Category) => void
active: string
}> = ({ value, handlerFilterCategory, active }) => {
let className = 'capitalize cursor-pointer hover:text-orange'
if (active === value) className += ' text-orange'
return (
<li className={className} onClick={() => handlerFilterCategory(value)}>
{value}
</li>
)
}
const BlogNavbar: React.FC<{
handlerFilterCategory: (category: Category) => void
active: string
}> = props => {
return (
<div className="min-h-full ">
<h3 className="font-mono text-3xl font-medium tracking-wider text-purple">
Blog
</h3>
<div className="flex flex-col mt-10 space-x-3 space-y-6 overflow-x-auto list-none">
<NavItem value="dog" {...props} />
<NavItem value="Border Collie" {...props} />
<NavItem value="bulldog" {...props} />
<NavItem value="husky" {...props} />
</div>
</div>
)
}
export default BlogNavbar
Key Points
The BlogNavbar component receives handlerFilterCategory and active as props from the index component. These are then spread as props to each NavItem component.
The NavItem component receives handlerFilterCategory, active, and value via prop drilling. The value is passed as an argument to handlerFilterCategory, and if the strings active and value match, the text is colored.
With these changes, we have enabled displaying articles based on article type.
Adding Dark Mode
The steps for introducing dark mode are described in detail in the official Tailwind CSS documentation.
Following the official guide, to enable dark mode in Tailwind CSS, you need to modify the tailwind.config.js file.
module.exports = {
purge: ['./pages/**/*.{tsx}', './components/**/*.{tsx}'],
- darkMode: 'false',
+ darkMode: 'class',
......
}
You also need to install one library.
$ npm install next-themes
# or
$ yarn add next-themes
After installing the library, import ThemeProvider into _app.js.
import React from 'react'
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import Head from 'next/head'
+import { ThemeProvider } from 'next-themes'
function MyApp({ Component, pageProps }: AppProps): React.ReactNode {
return (
<>
<Head>
<title>Next Blog</title>
<link rel="icon" href="/favicon.ico" />
</Head>
+ <ThemeProvider attribute="class">
<Component {...pageProps} />
+ </ThemeProvider>
</>
)
}
export default MyApp
Now, simply apply styles by prefixing them with dark: where you want dark mode to be applied.
•import React from 'react'
import React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Kaushan+Script&display=swap"
rel="stylesheet"
/>
</Head>
+ <body className="bg-fixed bg-gradient-to-r from-orange to-purple dark:from-subtext dark:to-darkblue dark:text-white">
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
Additionally, we will add a feature to switch between dark and light modes by clicking a button to the Sidebar component.
import React from 'react'
+import { useTheme } from 'next-themes'
import { Category } from '../../types'
+import { SunIcon } from '@heroicons/react/outline'
+import { MoonIcon } from '@heroicons/react/solid'
export const NavItem: React.FC<{
value: Category
handlerFilterCategory: (category: Category) => void
active: string
}> = ({ value, handlerFilterCategory, active }) => {
let className = 'capitalize cursor-pointer hover:text-orange'
if (active === value) className += ' text-orange'
return (
<li className={className} onClick={() => handlerFilterCategory(value)}>
{value}
</li>
)
}
const BlogNavbar: React.FC<{
handlerFilterCategory: (category: Category) => void
active: string
}> = props => {
+ const { theme, setTheme } = useTheme()
+ const changeTheme = () => {
+ setTheme(theme === 'light' ? 'dark' : 'light')
+ }
return (
<div className="flex flex-col justify-between min-h-full">
<div className="flex justify-between">
<ch3 className="text-3xl font-medium tracking-wider text-purple font-kaushan dark:text-subtext">
Blog
</h3>
+ {theme === 'light' ? (
+ <SunIcon className="h-8" />
+ ) : (
+ <MoonIcon className="h-8" />
+ )}
</div>
<div className="flex flex-col py-6 space-x-3 space-y-3 overflow-x-auto list-none">
<NavItem value="dog" {...props} />
<NavItem value="Border Collie" {...props} />
<NavItem value="bulldog" {...props} />
<NavItem value="husky" {...props} />
</div>
+ <button
+ onClick={changeTheme}
+ className="py-2 my-4 text-white bg-black rounded-full cursor-pointer bg-gradient-to-r from-orange to-purple dark:from-subtext dark:to-darkblue focus:outline-none hover:scale-105"
+ >
+ {theme === 'light' ? 'Light Mode' : 'Dark Mode'}
+ </button>
</div>
)
}
export default BlogNavbar
With these changes, dark mode has been introduced.
Deploying to Vercel
There are minimum requirements for deploying to Vercel.
Click here to log in to Vercel:
Steps to deploy after logging in
- Log in to Vercel.
- Click "New Project".
- Import Git Repository (select the repository you want to deploy).
- Select Vercel Scope (select your GitHub account).
Finally, "Import Project".
If environment variables are required, as in this case, copy and paste them from your .env file.
Then, click "Deploy" and it will commend you with "Congratulations!"
(I forgot to take a screenshot...)
This completes the deployment to Vercel.
That's all for this article.
Thank you for reading this far!!
Discussion