更新情報

こんにちは。なかあつです。

多趣味なこともあり前々からブログをやりたかったのですが、1つのサービスにいろんなカテゴリの記事を投稿するのはどうなのだろう…と思い始めていたこともありどのように始めようかなと悩んでいました。
どこに落ち着こうか悩んで一旦放置した後、デザインについて勉強し始めたタイミングでnakaatsu.comを個人サイトとしてブログを載せるようにアップデートし、ポートフォリオサイト兼個人ブログとして作成することにしました。

https://www.nakaatsu.com/

今回は作成したサイトの構成や、こだわった点について書いていきます。

構成

フレームワーク

フレームワークはNext.jsのApp Routerを習熟したくそちらを採用しました。従来型のPages Routerは以前から利用していましたが、App Routerを0から使うのは初めてです。

ヘッドレスCMS

コンテンツ管理のためにmicroCMSを使用しました。サーバーやDBを用意・管理することなくデータ管理を行えるヘッドレスCMSは大変便利です。
microCMSは機能が充実しているリッチエディタがあったり、プロジェクトへ導入しやすいSDKが公式で提供されていて今回の目的に合致しているため採用しました。とても便利なのでぜひ皆さん使ってみてください。

ホスティング

Next.jsを採用することもあり、相性の良いVercelをホスティング先にしました。Cloudflare Pagesなども試したかったのですが、App Routerの効果を最大限発揮するならVercelなのかな…と一旦思考停止してこちらを採用しました。

デザイン・スタイル

サイトデザインについては自分で考えて、スタイルはCSS Modulesでゴリゴリ実装することにしました。
シンプルなデザインが好みなのであまりごちゃつかないようにしつつ、寂しくないように気をつけてデザインしました。

以下参考文献です。どちらもデザイニングの基礎から実務ベースでのデザインの考え方まで体系的に学ぶことができるのでエンジニアの方からデザイナーの方まで幅広くおすすめです。

  1. Web Designing 2024年2月号
  2. ロゴデザイン研究 100の実例に学ぶ最適解を探し出すアプローチ

アニメーションライブラリ

Webデザインはワイヤーフレーム通りに表示するのみでなく、端末上で再生されるアニメーションも大事な要素であると考えているためリッチなアニメーションを導入したく、Framer Motionを採用しました。
Framer MotionはReact用のアニメーションライブラリで、シンプルな記述でリッチなアニメーションを実現できます。

サイト作成手順

Next.js(App Router), microCMS

microCMSをNext.jsのApp Routerで利用するための手順について、microCMSドキュメントにまとまっているのでぜひこちらを参考にしてください。
より詳しいセットアップについてはmicroCMS公式ブログの記事が充実しているのでおすすめです。

Framer Motion

まずnpmやyarnでプロジェクトに導入します。

yarn add framer-motion

アニメーションを付ける要素をラップするコンポーネントを作成します。今回は「全体がふわっと表示」するアニメーションをどのページでも実行するために、initial={{ opacity: 0 }}からanimate={{ opacity: 1 }}に変わるように設定しました。

'use client'

import { AnimatePresence, motion } from "framer-motion"
import { usePathname } from "next/navigation"
import React from "react"

const MotionWrapper = ({ children }: { children: React.ReactNode }) => {
  const pathName = usePathname()

  return (
    <AnimatePresence mode="wait">
      <motion.div key={pathName} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
        {children}
      </motion.div>
    </AnimatePresence>

  )
}

export default MotionWrapper

このラッパーをpage.tsxにつけることで画面全体にこのアニメーションを反映されることができます。

const Home = async () => {
  const res = await getBlogList({ limit: 6, orders: '-publishedAt' })

  return (
    <MotionWrapper>
      <ProgressBar />
      <WhatIs />
      <TopBlogs contents={res.contents} totalCount={0} limit={0} offset={0} />
      <Footer />
    </MotionWrapper>
  )
}

export default Home

これで<MotionWrapper>を設定しているページは全体がふわっと表示されるようになりました。

他にくるっと回転するアニメーションを設定したり

<Link href={'https://twitter.com/nakaatsu'} rel="noopener noreferrer" target="_blank">
  <motion.div whileHover={{ scale: 1.2, rotate: 360, transition: { duration: 0.3 } }} whileTap={{ scale: 0.8 }}>
    <TwitterOutlined className={`${styles.actionIcon} ${styles.twitter}`} />
  </motion.div>
</Link>

ぽよんと現れるアニメーションを個別に設定しました。type: springでアニメーションに慣性が乗るようになっています。

<motion.div initial={{ x: 48, y: 48, scale: 0 }} whileInView={{ x: 0, y: 0, scale: 1 }} transition={{ duration: 0.4, delay: 0.1 * i, type: 'spring', bounce: 0.3 }} viewport={{ margin: '120px' }}>
   <motion.div whileHover={{ scale: 1.05, transition: { duration: 0.3 } }} transition={{ type: "spring", stiffness: 400, damping: 11 }} whileTap={{ scale: 0.9 }}>
    <div className={styles.imageContainer}>
      <img src={blog.eyecatch?.url + '?fit=crop&w=480&h=480'} />
       {blog.category &&
        <div key={blog.category.id} className={styles.category}>{blog.category.name}</div>
       }
    </div>
    <div className={styles.text}>
      <time>{blog.publishedAt ? formatDate(blog.publishedAt) : ''}</time>
      <h2>{blog.title}</h2>
    </div>
   </motion.div>
</motion.div>

アニメーションが開始される条件を視界(ビューポート)に入ったときにしたり、ホバー時、タップ時で条件を分けたりと細かい設定ができます。
アニメーション適用箇所を<motion.div>のように設定するため影響箇所が明示的でわかりやすく、既存プロジェクトへの導入もtsxファイルを変更するのみで対応できるため導入しやすいライブラリだと思いました。
公式のExampleも充実しているので習熟も簡単でした。

Vercel

microCMSのWebhook設定で、ブログ記事を公開や削除したときにVercelに連携してデプロイされるようにします。
https://document.microcms.io/manual/webhook-setting

  1. Vercelの Project Settings -> Git -> Deploy Hooks からWobhook URLを発行
  2. microCMSのAPI設定 -> Webhook -> Vercel を選択し更新する条件を設定

これでmicroCMSのコンテンツ編集を行うとVercelのデプロイが自動で走るようになるため、今後はブログ記事をmicroCMSで公開するだけでサイト側も更新されるようになります。

こだわった点

サイトデザイン

トンマナを合わせる

デザインのトンマナを意識して合わせることで、サイト全体の説得力をもたせました。

  1. メインカラー(アクセントカラー)を1色のみとし、黒色・白色をそれぞれ複数持ちつつ優先度を決めて登場頻度を調整
  2. 要素の間隔を基本8px単位にし、間隔の強弱に意味をもたせつつ一定の秩序を設ける
    1. ブログ記事など細かい見た目に拘る箇所はより細かい調整を行う
  3. フォントサイズを大(16px)中(14.8px)小(12px)を基本とする
    1. h1やh2はブログ記事ページを基本としてサイト全体を合わせる

アクセント

サイト全体に吹き出し状のアクセントを配置しました。全体的に可愛いデザインになったかな…と思います。
またメインカラーとしてビビッドめな黄色を要所に使っています。ビビッドなアクセントを使うことで、背景を柔らかいグレーにして全体的にシックにしつつ大人しすぎないようになったかと思います。

アニメーション

  • サイト上部にアクセントカラーのスクロールバーを表示
  • 飛び出すようなアニメーションを全体的に配置しポップな印象に

つまづいた点

generateStaticParams

今回App Routerを用いてVercelに静的サイトをデプロイしたのですが、ブログの子ページに移動するとき遷移が遅いな…となっており原因を探すのに少し時間がかかりました。
原因としていはgenerateStaticParamsの設定が抜けていました。
https://nextjs.org/docs/app/api-reference/functions/generate-static-params

これはPages RouterにおけるgetStaticPathsと同じく、動的ルーティングのページを静的に生成するためのルートを指定するものになります。
今回だと[blogId]/page.tsx のコードは以下のようになります。

type Props = {
  params: {
    blogId: string;
  }
}

export const generateStaticParams = async () => {
  const { contents } = await getBlogList();

  const paths = contents.map((blog) => {
    return {
      blogId: blog.id,
    }
  });

  return [...paths]
}

const BlogPage = async ({ params }: Props) => {
  const { blogId } = params
  const res = await getBlogDetail(blogId)

  return (
    <MotionWrapper>
      <ProgressBar />
      <Card>
        <CardHeader iconPath='/images/notebook.svg' iconAlt='blog' title={res.title} link={''} isShare shareTitle={res.title} />
        <div className={styles.blogPageContainer}>
          <div className={styles.infoContainer}>
            <time>{res.publishedAt ? formatDate(res.publishedAt) : ''}</time>
            <div className={styles.category}>{res.category?.name}</div>
          </div>
          <div className={styles.imageContainer}>
            <img src={res.eyecatch?.url + '?w=960'} />
          </div>
          <div className={styles.content} dangerouslySetInnerHTML={{ __html: `${res.content}` }} />
        </div>
      </Card>
      <Footer />
    </MotionWrapper>
  )
}

export default BlogPage
  1. generateStaticParams でブログ記事一覧の blogId を取得
  2. BlogPage で blogId の記事情報を取得

という流れでルートが指定され静的ページが生成されるようになります。App Routerの場合にこれが必要となることを今回学べました。

'use client'

App Router では、Client ComponentとServer Componentを明確に区別するためにClient Componentに'use client'と指定することが必須になっています。
今回だとFramer Motionを実装している<motion.div>があるComponentにこの指定が必要なのですが、指定しない場合にでるエラー文が直接'use client'に言及されておらず気づくまでに時間がかかってしまいました。

Client ComponentとServer Componentを明確に区別する

ここを前提としてApp Routerを用いるならこのつまづきは起きなかっただろうなと思いました。もう少しApp Routerについて勉強してから実装進めるべきだったかな…と反省しました。

next/imageのpriority

<img>タグに当たる部分をnext/imageの<Image>で実装していたのですが、ページロード後にすぐ表示されずアニメーションが終わってから画像が表示されることがありました。
これはimageのpreload設定に当たるpriorityがデフォルトだとfalseになっており、ページ遷移後にロードされるようになっていました。そのためファーストビューに表示される要素は以下のようにprioriyを指定しました。

<Image src='https://images.hoge.nakaatsu_top.png?fit=crop&w=1200' alt="main Image" fill style={{ objectFit: 'cover' }} priority />

さいごに

きちんとデザインについて学んだ上でウェブサイトを制作した&アニメーションライブラリを活用してかわいいデザインにすることができたこともあり、自分としては結構満足して開発ができました。

他のユーザーを意識するようなブログ運営はしたくなかったこともあり個人サイトに落ち着いたのですが、サイト作成している間にしずかなインターネットさんがリリースされてこれで良いのではとちょっと思いました。
App Routerやデザインの習熟も目的として生まれたので、結果としては個人サイトを制作してよかったなと思いました。

2024年はアウトプット量を増やしていきたいので、今後もブログ記事や個人制作をがんばっていきます。