Jamstackなブログを作ろう #3

2023年07月11日

Jamstackなブログを作ろう #3

さて、3回目です。

それとなく完成形が見えてきた感じです。

それではいきましょう。

今回は肝となる、ブログの記事内容表示部分です。

Next.jsの目玉と言っても過言ではない、SSGに関する部分があります。

app/blog/[id]

このようなディレクトリを作成します。

idの周りの[]も必要です。

これがあることで、<app/blog/hogehoge>とか、<app/blog/fugafuga>のようなURLでアクセスがあった場合、このディレクトリが読み込まれます。

ここも、他のディレクトリ同様、layout.tsxと対になるpage.tsxを設置します。

それぞれ中身を見てみましょう。

export default async function BlogDetailLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <div>{children}</div>;
}

layout.tsxはなにもありません。

では、page.tsxを見てみましょう。

import { getBlogDetail, getBlogs } from "@/libs/microcms";
import { BlogPostType } from "@/types/blogPost";
import Image from "next/image";
import React from "react";
import parse from "html-react-parser";
import { format } from "date-fns";
import styles from "../../styles/page.module.css";
import Link from "next/link";
import "../../styles/code.css";

import { ArrowUturnLeftIcon } from "@heroicons/react/24/solid";

import { M_PLUS_Rounded_1c } from "next/font/google";

const mPlus400 = M_PLUS_Rounded_1c({
  weight: ["400"],
  subsets: ["latin"],
  display: "swap",
});

// for SSG
export async function generateStaticParams() {
  const contents = await getBlogs();

  const id = contents.map((content) => {
    return {
      id: content.id,
    };
  });
  return [...id];
}

export default async function BlogDetail({
  params: { id },
}: {
  params: { id: string };
}) {
  const blogDetail: BlogPostType = await getBlogDetail(id);
  return (
    <>
      <div className={mPlus400.className}>
        <div className={styles.detailArea}>
          <div className={styles.detailTitle}>
            <h1>{blogDetail.title}</h1>
          </div>
          <div className={styles.detailDate}>
            <p>{format(new Date(blogDetail.createdAt), "yyyy年MM月dd日")}</p>
          </div>
          <div className={styles.detailImage}>
            <Image
              src={blogDetail.eyecatch?.url || "/no-image.png"}
              //   width={320}
              //   height={200}
              layout="fill"
              objectFit="contain"
              alt={blogDetail.title}
            />
          </div>
          <div className={styles.detailText}>{parse(blogDetail.content)}</div>
          <div>
            <Link href="/blog" className={styles.detailBack}>
              <ArrowUturnLeftIcon className="w-5 h-4 m-0 p-0" />
              <span>戻る</span>
            </Link>
          </div>
        </div>
      </div>
    </>
  );
}

この中で大切なのが、generateStaticParamsです。

Next.js 12までは、getStaticPathsとgetStaticPropsを使ってSSGを実装していましたが、Next.js 13では、こちらを使います。

戻り値が今までと異なりますのでご注意ください。

これで、各ブログ記事は、ビルド時に静的に生成されます。

結果、リクエストがあった際には、生成済みHTMLだけがレスポンスで返されます。

表示、めっちゃ早いです。

not foundはこちら

もし、存在しないページにアクセスがあった場合は、自動的にapp/blog/id]/not-found.tsxが読まれます。

今は何も書いていませんが、適当にカスタマイズしてみてください。

import React from "react";

const NotFound = () => {
  return <div>blog not found...</div>;
};

export default NotFound;

中身はホント空です。returnの中に、not found時に表示したい内容を書きましょう。

私の場合、今後ゆっくりと実装していこうと思います。

一応完成

さて、これでブログ部分については、一応完成です。

CSSについては、いろいろと考え方があるかと思いますので、どれが正解とかありませんが、以下に貼っておきます。

tailwind CSSを使用していますので、合わせてご理解いただければと思います。

/* blog list page for pc */
@media screen and (min-width: 750px) {
  .topTitle {
    font-size: 5rem;
  }
  .topSubTitle {
    font-size: 2rem;
    text-align: center;
  }
  .topArrow {
    /* text-align: center; */
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .aboutMain {
    width: 70%;
    text-align: left;
    margin: 0 auto;
  }
  .topHeroImage > img {
    position: static !important;
    width: 100% !important;
    height: auto !important;
    margin: 0 auto;
  }

  .aboutTitle {
    font-size: 3rem;
  }
  .aboutArrow {
    /* text-align: center; */
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .aboutPara {
    line-height: 1.7rem;
  }

  .blogCard {
    display: block;
    margin: 30px;
    width: 100%;
    border: 1px solid #ccc;
    border-radius: 20px;
    box-shadow: 0px 3px 5px -1px #999;
    transition: all 0.5s ease-in-out;
  }
  .blogCard:hover {
    box-shadow: none;
  }

  .cardWrap {
    display: flex;
  }

  .cardWrap .cardThumnail img {
    border-radius: 20px 0 0 20px;
  }

  .cardWrap .cardText {
    display: block;
    width: 100%;
    position: relative;
  }

  .cardWrap .cardTitle {
    font-size: 2rem;
    margin-top: 1rem;
    margin-left: 0.7rem;
  }

  .cardWrap .createdate {
    font-size: 0.9rem;
    color: #ccc;
    position: absolute;
    bottom: 0;
    left: 0.7rem;
  }

  .navbar {
    height: 5rem;
    text-align: center;
    background-color: antiquewhite;
    line-height: 5rem;
    position: fixed;
    width: 100%;
    z-index: 9999;
  }
  .navText {
    font-size: 2rem;
    display: inline;
    margin-left: 3rem;
  }
  .navText.firstNav {
    margin-left: 0;
  }

  .footer {
    height: 5rem;
    line-height: 5rem;
    text-align: center;
    background-color: antiquewhite;
  }

  .blogListTitle {
    font-size: 2rem;
    margin-bottom: 0;
  }

  .detailArea {
    padding-top: 6rem;
    width: 100%;
    text-align: center;
  }
  .detailTitle {
    font-size: 2rem;
    position: relative;
    padding-bottom: 20px;
    font-size: 26px;
    text-align: center;
  }
  .detailTitle:after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 10px 6px 0 6px;
    border-color: #b99a00 rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
  }
  .detailDate {
    font-size: 0.9rem;
  }
  .detailImage > img {
    position: static !important;
    width: 70% !important;
    height: auto !important;
    margin: 3rem auto;
  }
  .detailText {
    width: 70%;
    margin: 0 auto;
    text-align: left;
    line-height: 1.6rem;
  }
  .detailText h1 {
    font-size: 3rem;
    margin: 1.5rem 0;
  }
  .detailText > h2 {
    font-size: 1.2rem;
    margin: 1rem 0;

    position: relative;
    padding-bottom: 10px;
    font-size: 26px;
  }
  .detailText > h2:after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 8px;
    background-image: repeating-linear-gradient(
      45deg,
      #b4a983 0px,
      #b4a983 1px,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, 0) 50%
    );
    background-size: 8px 8px;
  }
  .detailBack {
    display: flex;
    justify-content: center;
    align-items: center;
  }
}

@media screen and (max-width: 749px) {
  .navbar {
    height: 2rem;
    background-color: bisque;
    text-align: center;
    line-height: 2rem;
  }
  .navbar a {
    margin-left: 1rem;
  }
  .main {
    padding: 5px !important;
  }
  .topHeroImage > img {
    position: static !important;
    width: 100% !important;
    height: auto !important;
    margin: 20px auto;
  }
  .topTitle {
    text-align: center;
    font-size: 1.5rem;
  }
  .topSubTitle {
    text-align: center;
  }
  .topArrow {
    display: none;
  }
  .topPMargin {
    margin-top: 3rem;
  }
  .aboutMain {
    padding: 5px !important;
  }
  .blogListCard {
    margin-top: 3rem;
  }
  .cardTitle {
    font-size: 1.5rem;
  }
  .createdate {
    font-size: 0.9rem;
    color: #777;
  }
  .detailArea {
    width: 75%;
  }
  .detailTitle {
    font-size: 2rem;
  }
  .detailDate {
    margin-bottom: 1rem;
  }
  .detailImage > img {
    position: static !important;
    width: 100% !important;
    height: auto !important;
    margin: 20px auto;
  }
  .detailArea {
    width: 90%;
    margin: 0 auto;
  }
  .detailText h2 {
    font-size: 1.3rem;
    margin: 1rem 0;
  }
}

あとは、ヘッダーやらフッターやらありますが、これも各自で実装していただければと思います。

今後に向けて

さて、今後に向けての積み残しです。

  • カテゴリでの検索
  • ページネーション
  • metaタグ
  • フォーム

このあたりを実装していこうと思っています。

では、今回はmicroCMSとReact + Next.js13でのブログ作成までの道のりでした。

ありがとうございました。

戻る