簡易ブログを作ってみた!パート2(with Next.js & contentful)
はじめに
今回の記事は、前回の続きとなります。前回記事も是非、ご覧ください!!
本記事の目標
-
Tailwind CSS
導入 - 記事の種類によって、表示を変える
- ダークモード追加
-
vercel
にデプロイ
この4点の達成を目指します。
↓ 完成品
Tailwind CSS
導入
スタイルに Tailwind CSS
を導入します。Next.js
をベースとする Tailwind CSS
の導入方法は公式で述べられています。
また Tailwind CSS
はカスタマイズも簡単です。
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: []
}
また vscode
の拡張機能で Color Highlight をインストールしておけば、色が視覚化できます。
一例ですが、簡潔にスタイリングできます。
import React from 'react'
const index = () => {
return (
<div className="flex w-full mt-5 justify-evenly">
<p className="w-5 h-5 rounded-full animate-bounce bg-orange" />
<p className="w-5 h-5 rounded-t-sm animate-spin bg-blue" />
<p className="w-5 h-5 rounded-full animate-ping bg-purple" />
<p className="w-5 h-5 rounded-full animate-pulse bg-subtext" />
<p className="w-5 h-5 rounded-full animate-none bg-darkblue" />
</div>
)
}
export default index
コードを書く
先程掲げた残り3つの目標を成すために、下準備を行います。
まずは 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
SSG によって Next.js
と contentful
との接続を行います。こちらは前回記事を御覧ください。ただ前回記事は、props の型推論がうまくいっておらず、 any 型となっていたため修正を加えています。(前回記事も修正済みです。)
見た目は以下のとおりです。
以降、左側を Sidebar コンポーネント、右側を BlogCard コンポーネントとします。
BlogCard コンポーネント
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"
/>
<p className="my-2 text-center">{item.fields.title}</p>
</>
)
}
export default BlogCard
画像には Next.js
の Image タグを使います。今回のように外部(contentful)ファイルを読み込む場合、next.config.js ファイルを作成し以下のように追記する必要があります。
module.exports = {
images: {
domains: ['images.ctfassets.net'] // ←外部ファイルによって記述が変わります
}
}
また各記事の画像をクリックすると、画像が拡大し、さらに detail(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"
/>
<p 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}>
+ <a className="flex items-center px-4 space-x-3 text-2xl +font-bold bg-gray-200 hover:text-orange">
+ detail
+ </a>
+ </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"
+ >
+ <XIcon className="h-8" />
+ </button>
+ </div>
+ )}
</>
)
}
export default BlogCard
アイコンには heroicons
を使っています。
因みに、 item.fields.type(画像では[dog],[husky])というのは、contentful
で作成した Content model の一つです。次のチャプターでも用います。
記事の種類によって、表示する記事を変える。
下準備は以上で、目標の2つ目に入ります。
まず、Sidebar コンポーネントと BlogCard コンポーネントとの連携を図るために index コンポーネントに変更を加えていきます。
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
ポイント
まず useState 2つ用意します。
1つ目の useState は contentful
から取得したデータ(article)を引数に取り、条件によって article の内容を更新します。それを担うのが handlerFilterCategory 関数で、引数(category)によって article の全てを必要とするのか、一部を必要とするのかを分けます。
一部を必要とする場合、filter メソッドによって配列処理を行います。contentful で作った各記事の type に handlerFilterCategory 関数の引数(category)が含まれている場合 article の内容を更新しています。それを newArray と定め、 setState(setArticles) によって状態管理を行います。
後は、最新の state の値(articles)を BlogCard コンポーネントに props 経由で渡すといった具合です。
2つ目の useState はTailwind CSS
によるスタイリングのために用意しました。次の Sidebar コンポーネントのコードで紹介致します。
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
ポイント
BlogNavbar コンポーネントの引数には、先程 index コンポーネントで props として渡した handlerFilterCategory と active を持ちます。さらに各 NavItem コンポーネントにスプレッド構文で渡します。
NavItem コンポーネントでは、バケツリレーで handlerFilterCategory と active と value を受け取ります。 handlerFilterCategory の引数には value が渡り、 active と value との文字列が一致した場合、テキストに色付けます。
以上の変更で、 記事の種類によって、表示する記事を変える を可能にしました。
ダークモード追加
ダークモードの導入手順は Tailwind CSS
公式で詳しく述べられています。
公式を参考に、
Tailwind CSSでタークモードを有効化するためには、 tailwind.config.js ファイルを修正します。
module.exports = {
purge: ['./pages/**/*.{tsx}', './components/**/*.{tsx}'],
- darkMode: 'false',
+ darkMode: 'class',
......
}
また一つライブラリをインストールする必要があります。
$ npm install next-themes
# or
$ yarn add next-themes
ライブラリをインストールしたら、_app.js に ThemeProvider を import します。
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
あとはダークモードを適用したい場所に dark:
に続きスタイルを付与するだけです。
•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
またボタンをクリックすると、ダークモード、ライトモードと入れ替わる機能を Sidebar コンポーネントに追加します。
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">
<h3 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
以上の変更で、ダークモードを導入できました。
vercel にデプロイ
vercel にデプロイするには、最低限必要なことがあります。
vercel にログインするには以下をクリック
ログイン後、デプロイするまでの手順
- vercel にログイン
- Nex Project クリック
- Import Git Repository(デプロイしたいレポジトリをセレクト)
- Select Vercel Scope(github アカウントをセレクト)
最後に、Import Project
今回のように、環境変数が必要な場合、env ファイルよりコピペしてきます。
後は deploy をクリックすると、 Congratulations! と称賛してくれます。
(スクショを撮るの忘れました...)
これにより、 vercel にデプロイできました。
以上になります。
ここまで読んでいただきありがとうございました!!
Discussion