ファーストビュー画像
ヘッダーロゴ
ホームアイコン
>
>
【Nextjs15】microCMSとAIを使って技術ブログを作成する 後編
ブログ構築

【Nextjs15】microCMSとAIを使って技術ブログを作成する 後編

作成日2025/01/14
更新日2025/01/18
アイキャッチ
# Next.js
# microCMS

前回は静的にブログページを作成、microCMSの設定を行いました。
前回の記事はこちら

今回は実際にmicroCMSと接続をしてブログを完成させていきます。

microcms-js-sdkのインストール

microCMSのAPIとの通信にはmicroCMS公式が公開しているmicrocms-js-sdkを利用します。

$ pnpm add microcms-js-sdk 

環境変数を設定

microCMSとの通信にはサービスドメイン、APIキーが必要です。

サービスドメインはサービスページ左上で確認できます。

APIキーは権限管理1個のAPIキーから確認できます。

プロジェクトルートに.env.localファイルを作成してそれぞれ設定します。

MICROCMS_SERVICE_DOMAIN=xxxxxxxx
MICROCMS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx

接続用のクライアント作成

libフォルダにclient.tsファイルを作成して、microCMSとの通信の処理を書いていきます。

import { createClient } from "microcms-js-sdk";

if (!process.env.MICROCMS_SERVICE_DOMAIN) {
  throw new Error("サービスドメインが必要です");
}

if (!process.env.MICROCMS_API_KEY) {
  throw new Error("APIキーが必要です");
}

const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
  apiKey: process.env.MICROCMS_API_KEY,
});

サービスドメインとAPIキーを指定して接続用のクライアントを定義しました。
このクライアントを使ってカテゴリ、記事を取得していきます。

カテゴリを取得

定義したクライアントを使ってカテゴリを取得します。

// 省略

// ↓追加↓
export type Category = {
  id: string;
  name: string;
};
// ↑追加↑

const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
  apiKey: process.env.MICROCMS_API_KEY,
});

// ↓追加↓
export async function getCategories() {
  const categories = await client.getList<Category>({
    endpoint: "categories",
  });

  return categories;
}
// ↑追加↑

カテゴリ一覧を取得する処理を追加しました。
endpointの箇所に指定する値は該当のAPIのAPI設定エンドポイントから確認・編集ができます。

静的に表示していたカテゴリの部分をAPIで動的に表示するよう変更します。

import { getCategories } from "@/lib/client"; // 追加
import { Button } from "./ui/button";

export default async function CategoryFilter() {
  const { contents: categories } = await getCategories(); // 変更

  return (
    <div className="mb-8">
      <h2 className="mb-4 text-xl font-semibold">Categories</h2>
      <div className="flex flex-wrap gap-2">
        <Button>ALL</Button>
        {/* ↓変更↓ */}
        {categories.map((category) => (
          <Button variant="outline" key={category.id}>
            {category.name}
        {/* ↑変更↑ */}
          </Button>
        ))}
      </div>
    </div>
  );
}

microCMSのAPIレスポンスは取得する値が contents という配列で返されるため、contentscategoriesという変数で受け取るようにしています。

起動して次のようにmicroCMSで作成したカテゴリが表示されていれば正常に取得できています。

記事を取得

カテゴリと同じように記事を取得する処理を追加します。

import {
  createClient,
  MicroCMSDate, // 追加
  MicroCMSImage, // 追加
  MicroCMSQueries, // 追加
} from "microcms-js-sdk";

if (!process.env.MICROCMS_SERVICE_DOMAIN) {
  throw new Error("MICROCMS_SERVICED_DOMAIN is required");
}

if (!process.env.MICROCMS_API_KEY) {
  throw new Error("MICROCMS_API_KEY is required");
}

export type Category = {
  id: string;
  name: string;
};

// ↓追加↓
export type Article = {
  id: string;
  title: string;
  content: string;
  eyecatch?: MicroCMSImage;
  category: Category;
} & MicroCMSDate;
// ↑追加↑

const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
  apiKey: process.env.MICROCMS_API_KEY,
});

export async function getCategories() {
  const categories = await client.getList<Category>({
    endpoint: "categories",
  });

  return categories;
}

// ↓追加↓
export async function getArticles(queries?: MicroCMSQueries) {
  const articles = await client.getList<Article>({
    endpoint: "articles",
    queries,
  });

  return articles;
}
// ↑追加↑

記事一覧は後ほどカテゴリごとに絞り込めるようにするため、関数の引数で検索条件であるqueriesを受け取れるようにしています。
エンドポイントはデフォルトではblogsとなっていますが、今回はarticlesに変更しています。

静的に表示していた記事一覧をAPIで動的に表示するよう変更します。

記事一覧コンポーネントをarticles変数を受け取って表示するようにします。

import Link from "next/link";
import { Card, CardContent, CardFooter } from "./ui/card";
import { Badge } from "./ui/badge";
import { Article } from "@/lib/client"; // 追加

type Props = {
  articles: Article[];
};

export default function ArticleList(props: Props) {
  return (
    <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
      {props.articles.map((article) => ( // 変更
        <Link href="#" key={article.id}>
          <Card className="overflow-hidden">
            <div className="relative h-48 border">Image</div>
            <CardContent className="p-4">
              <Badge>{article.category.name}</Badge> {/* 変更 */}
              <h2 className="text-xl font-semibold">{article.title}</h2>
            </CardContent>
            <CardFooter className="text-sm text-slate-600">
              {article.publishedAt} {/* 変更 */}
            </CardFooter>
          </Card>
        </Link>
      ))}
    </div>
  );
}

TOPページ側で記事を取得して記事一覧コンポーネントに渡してあげます。

import ArticleList from "@/components/article-list";
import CategoryFilter from "@/components/category-filter";
import Pagination from "@/components/pagination";
import { getArticles } from "@/lib/client";

export default async function Home() {
  const { contents: articles } = await getArticles();

  return (
    <div>
      <CategoryFilter />
      <ArticleList articles={articles} />
      <Pagination />
    </div>
  );
}

次のようにmicroCMSで作成した記事が表示されたら正常に取得できています。

画像を表示する

ブログAPIのアイキャッチに適当な画像を設定します。

アイキャッチは必須にしていないため、設定していない場合に表示する画像をpublicフォルダに置いておきます。
今回は以下の画像をno-image.pngというファイル名で保存しています。

記事一覧コンポーネントを修正します。

import Link from "next/link";
import { Card, CardContent, CardFooter } from "./ui/card";
import { Badge } from "./ui/badge";
import { Article } from "@/lib/client";
import Image from "next/image"; // 追加

type Props = {
  articles: Article[];
};

export default async function ArticleList(props: Props) {
  return (
    <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
      {props.articles.map((article) => (
        <Link href="#" key={article.id}>
          <Card className="overflow-hidden">
            {/* ↓修正↓ */}
            <div className="relative border">
              <Image
                className="w-full"
                src={article.eyecatch?.url ?? "/no-image.png"}
                width={345}
                height={240}
                alt="アイキャッチ"
              />
            </div>
            {/* ↑修正↑ */}
            <CardContent className="p-4">
              <Badge>{article.category.name}</Badge>
              <h2 className="text-xl font-semibold">{article.title}</h2>
            </CardContent>
            <CardFooter className="text-sm text-slate-600">
              {article.publishedAt}
            </CardFooter>
          </Card>
        </Link>
      ))}
    </div>
  );
}

これで再度ページにアクセスするとエラーが発生します。

このエラーはmicroCMSの画像URLがNext.jsの設定ファイルで許可させていないために起きています。
公式に従って設定ファイルを編集します。

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.microcms-assets.io",
      },
    ],
  },
};

export default nextConfig;

再度ページにアクセスすると正常に画像が表示されています。
3つ目の記事は画像を設定していないので、代わりの画像が表示されています。

投稿日の表示を修正

現状日付が細かい部分まで表示されてしまっているので修正します。

dayjsを追加します。

$ pnpm add dayjs

utils.tsファイル内に日付をフォーマットする関数を作成します。

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import dayjs from "dayjs"; // 追加
import timezone from "dayjs/plugin/timezone"; // 追加
import utc from "dayjs/plugin/utc"; // 追加

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// ↓追加↓
dayjs.extend(utc);
dayjs.extend(timezone);

export const formatDate = (date: string) => {
  const formattedDate = dayjs.utc(date).tz("Asia/Tokyo").format("YYYY/MM/DD");
  return formattedDate;
};

日付を表示している箇所をフォーマット関数を使うように修正します。

import Link from "next/link";
import { Card, CardContent, CardFooter } from "./ui/card";
import { Badge } from "./ui/badge";
import { Article } from "@/lib/client";
import Image from "next/image";
import { formatDate } from "@/lib/utils"; // 追加

type Props = {
  articles: Article[];
};

export default async function ArticleList(props: Props) {
  return (
    <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
      {articles.map((article) => (
        <Link href="#" key={article.id}>
          <Card className="overflow-hidden">
            <div className="relative border">
              <Image
                className="w-full"
                src={article.eyecatch?.url ?? "/no-image.png"}
                width={345}
                height={240}
                alt="アイキャッチ"
              />
            </div>
            <CardContent className="p-4">
              <Badge>{article.category.name}</Badge>
              <h2 className="text-xl font-semibold">{article.title}</h2>
            </CardContent>
            <CardFooter className="text-sm text-slate-600">
              {formatDate(article.publishedAt!)} {/* 修正  */}
            </CardFooter>
          </Card>
        </Link>
      ))}
    </div>
  );
}

これでYYYY/MM/DDの形式で表示されるようになりました。

記事詳細ページを作成

記事詳細ページのデザインを作ってもらうのを忘れていたのでv0にお願いします。

こんな感じで作ってくれたので参考に詳細ページを実装していきます。

記事詳細の取得

microCMSから記事の詳細情報を取得する関数を追加します。

// 省略
export async function getArticle(contentId: string) {
  const article = await client.getListDetail<Article>({
    endpoint: "articles",
    contentId,
  });

  return article;
}

タイポグラフィ設定

microCMSで作成した記事のコンテンツはHTML形式で取得できますが、h1タグ, pタグなどそれぞれにスタイルを当てていくのは大変なので@tailwindcss/typographyを使って一括でスタイルを当てるようにします。

$ pnpm add @tailwindcss/typography 

Taliwind CSSの設定ファイルに追加します。


  },
  plugins: [
    require("tailwindcss-animate"),
    require("@tailwindcss/typography") // 追加
  ],
} satisfies Config;

これでスタイルを当てたい要素にproseクラスを追加するだけでいい感じに記事らしくなります。

proseクラス未適応

proseクラス適応

詳細ページ作成

v0に作ってもらったデザインをもとに詳細ページを作成します。

articles/[articleId]フォルダにpage.tsxファイルを以下の内容で作成します。

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { getArticle } from "@/lib/client";
import { formatDate } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";

export default async function Page(props: {
  params: Promise<{ articleId: string }>;
}) {
  const { articleId } = await props.params;
  const article = await getArticle(articleId);

  if (!article) {
    notFound();
  }

  return (
    <article className="mx-auto max-w-3xl">
      <Button asChild className="mb-4">
        <Link href="/">← Back to all posts</Link>
      </Button>
      <div className="relative mb-8 h-[400px]">
        <Image
          src={article.eyecatch?.url ?? "/no-image.png"}
          alt={article.title}
          fill
          style={{ objectFit: "cover" }}
          className="rounded-lg"
        />
      </div>
      <h1 className="mb-4 text-4xl font-bold">{article.title}</h1>
      <div className="mb-6 flex items-center">
        <span className="mr-4 text-slate-600">
          {formatDate(article.publishedAt!)}
        </span>
        <Badge variant="secondary">{article.category.name}</Badge>
      </div>
      <div className="prose max-w-none">
        <div
          dangerouslySetInnerHTML={{
            __html: article.content,
          }}
        ></div>
      </div>
    </article>
  );
}

記事一覧から詳細ページにアクセスできるようにリンクを修正します。

import Link from "next/link";
import { Card, CardContent, CardFooter } from "./ui/card";
import { Badge } from "./ui/badge";
import { Article } from "@/lib/client";
import Image from "next/image";
import { formatDate } from "@/lib/utils";

type Props = {
  articles: Article[];
};

export default async function ArticleList(props: Props) {
  return (
    <div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
      {props.articles.map((article) => (
        <Link href={`/articles/${article.id}`} key={article.id}> {/* 詳細ページへのリンクに修正 */}
          <Card className="overflow-hidden">
            <div className="relative border">
              <Image
                className="w-full"
                src={article.eyecatch?.url ?? "/no-image.png"}
                width={345}
                height={240}
                alt="アイキャッチ"
              />
            </div>
            <CardContent className="p-4">
              <Badge>{article.category.name}</Badge>
              <h2 className="text-xl font-semibold">{article.title}</h2>
            </CardContent>
            <CardFooter className="text-sm text-slate-600">
              {formatDate(article.publishedAt!)}
            </CardFooter>
          </Card>
        </Link>
      ))}
    </div>
  );
}

これで詳細ページにアクセスができるようになりました。
一覧ページから記事をクリックして以下のように表示されたら完了です。

カテゴリごとのページを作成

記事をカテゴリごとに絞り込めるようにカテゴリごとのページを作成していきます。

カテゴリページではカテゴリコンポーネントで選択中のカテゴリがわかるように、
記事一覧コンポーネントで選択中のカテゴリの記事のみ表示できるように修正していきます。

カテゴリコンポーネントを以下のように修正します。

import { getCategories } from "@/lib/client";
import { Button } from "./ui/button";
import Link from "next/link"; // 追加

// ↓追加↓
type Props = {
  currentCategoryId?: string;
};
// ↑追加↑

export default async function CategoryFilter(props: Props) {
  const { contents: categories } = await getCategories();
  // ↓追加↓
  const currentCategoryId = props.currentCategoryId;
  const currentCategory = categories.find(
    (category) => category.id === currentCategoryId
  );
  // ↑追加↑

  return (
    <div className="mb-8">
      <h2 className="mb-4 text-xl font-semibold">
        {currentCategoryId ? currentCategory?.name : "Categories"}
      </h2>
      <div className="flex flex-wrap gap-2">
        {/* ↓修正↓ */}
        <Link href="/">
          <Button
            variant={currentCategoryId === undefined ? "default" : "outline"}
          >
            ALL
          </Button>
        </Link>
        {categories.map((category) => (
          <Link key={category.id} href={`/categories/${category.id}`}>
            <Button
              variant={
                category.id === currentCategoryId ? "default" : "outline"
              }
            >
              {category.name}
            </Button>
          </Link>
          {/* ↑修正↑ */}
        ))}
      </div>
    </div>
  );
}

カテゴリコンポーネントの引数に現在のカテゴリIDを渡せるようにして、該当のカテゴリボタンのスタイルを変更するように修正しています。

TOPページの場合はALLボタンのスタイルが変更されるようになっています。

また、TOPページではCategoriesという文字が表示されていた箇所はカテゴリ選択中の場合該当カテゴリ名が表示されるよう修正しています。

コンポーネントの修正ができたのでカテゴリページを作成していきます。

categories/カテゴリIDのようなパスにしたいので、詳細ページの時と同じように
categoriesフォルダ内に[categoryId]フォルダ、[categoryId]フォルダの中にpage.tsxファイルを以下の内容で作成します。

import ArticleList from "@/components/article-list";
import CategoryFilter from "@/components/category-filter";
import Pagination from "@/components/pagination";
import { getArticles } from "@/lib/client";

export default async function Page(props: {
  params: Promise<{ categoryId: string }>;
}) {
  const { categoryId } = await props.params;
  const { contents: articles } = await getArticles({
    filters: `category[equals]${categoryId}`,
  });

  return (
    <div>
      <CategoryFilter currentCategoryId={categoryId} />
      <ArticleList articles={articles} />
      <Pagination />
    </div>
  );
}

カテゴリコンポーネントと同じようにコンポーネントの引数にカテゴリIDを渡せるようにして記事一覧を取得する関数のfilterとして利用しています。

この状態でTOPページにアクセスして適当なカテゴリをクリックします。
以下のようにカテゴリボタンのスタイル変更、記事の絞り込みができていればカテゴリごとのページは完了です。

ページネーション機能を作成

今回は3件ずつのページネーションとします。
動作確認がしやすいようにmicroCMSで適当に記事を増やしておきます。

ページごとの記事数の定数を定義

ページごとの記事数である3という数値を一箇所で管理できるように定数を定義します。

libフォルダに以下の内容でconstants.tsファイルを作成します。

export const LIMIT = 3;

TOPページの記事数を制限

現在全ての記事が表示されているTOPページを3件のみ表示するように変更します。

import ArticleList from "@/components/article-list";
import CategoryFilter from "@/components/category-filter";
import Pagination from "@/components/pagination";
import { getArticles } from "@/lib/client";
import { LIMIT } from "@/lib/constants"; // 追加

export default async function Home() {
  const { contents: articles } = await getArticles({
    limit: LIMIT, // 追加
    offset: 0, // 追加
  });

  return (
    <div>
      <CategoryFilter />
      <ArticleList articles={articles} />
      <Pagination />
    </div>
  );
}

ページネーションコンポーネントを修正

静的に作成していたページネーションコンポーネントを以下のように修正します。

import Link from "next/link";
import { Button } from "./ui/button";
import { LIMIT } from "@/lib/constants";

type Props = {
  totalCount: number;
  currentPage?: number;
};

export default function Pagination(props: Props) {
  const pageCount = Math.ceil(props.totalCount / LIMIT);
  const currentPage = props.currentPage ?? 1;

  return (
    <div className="mt-8 flex items-center justify-center space-x-2">
      {Array.from({ length: pageCount }).map((_, i) => (
        <Link key={i} href={`/pages/${i + 1}`}>
          <Button variant={i + 1 === currentPage ? "default" : "outline"}>
            {i + 1}
          </Button>
        </Link>
      ))}
    </div>
  );
}

ページ数を計算するためのtotalCountと、現在のページのスタイルを変えるためのcurrentPageを渡せるようにしています。

ページ番号ごとのページを作成

カテゴリごとのページと同じようにpagesフォルダ内に[currentPage]フォルダ、[currentPage]フォルダの中にpage.tsxファイルを以下の内容で作成します。

import ArticleList from "@/components/article-list";
import CategoryFilter from "@/components/category-filter";
import Pagination from "@/components/pagination";
import { getArticles } from "@/lib/client";
import { LIMIT } from "@/lib/constants";

export default async function Page(props: {
  params: Promise<{ currentPage: string }>;
}) {
  const { currentPage } = await props.params;
  const currentPageInt = parseInt(currentPage, 10);
  const { contents: articles, totalCount } = await getArticles({
    limit: LIMIT,
    offset: (currentPageInt - 1) * LIMIT,
  });

  return (
    <div>
      <CategoryFilter />
      <ArticleList articles={articles} />
      <Pagination totalCount={totalCount} currentPage={currentPageInt} />
    </div>
  );
}

パスから現在のページを取得して記事取得時のoffset(記事の取得開始位置)を動的に変更しています。

TOPページでページネーションをクリックして各ページに遷移できるようになっていることを確認します。

1ページ目

2ページ目

カテゴリごとのページでのページネーション

現状カテゴリ指定した状態でのページネーションが機能しないので、修正していきます。

カテゴリページも3件ずつ表示されるようにします。

import ArticleList from "@/components/article-list";
import CategoryFilter from "@/components/category-filter";
import Pagination from "@/components/pagination";
import { getArticles } from "@/lib/client";
import { LIMIT } from "@/lib/constants"; // 追加

export default async function Page(props: {
  params: Promise<{ categoryId: string }>;
}) {
  const { categoryId } = await props.params;
  const { contents: articles, totalCount } = await getArticles({
    limit: LIMIT, // 追加
    offset: 0, // 追加
    filters: `category[equals]${categoryId}`,
  });

  return (
    <div>
      <CategoryFilter currentCategoryId={categoryId} />
      <ArticleList articles={articles} />
      <Pagination totalCount={totalCount} />
    </div>
  );
}

ページネーションコンポーネントで現在のパスを受け取れるように修正します。

import Link from "next/link";
import { Button } from "./ui/button";
import { LIMIT } from "@/lib/constants";

type Props = {
  totalCount: number;
  currentPage?: number;
  basePath?: string; // 追加
};

export default function Pagination(props: Props) {
  const pageCount = Math.ceil(props.totalCount / LIMIT);
  const currentPage = props.currentPage ?? 1;
  const basePath = props.basePath ?? ""; // 追加

  return (
    <div className="mt-8 flex items-center justify-center space-x-2">
      {Array.from({ length: pageCount }).map((_, i) => (
        <Link key={i} href={`${basePath}/pages/${i + 1}`}> {/* 修正 */}
          <Button variant={i + 1 === currentPage ? "default" : "outline"}>
            {i + 1}
          </Button>
        </Link>
      ))}
    </div>
  );
}

これにより、現在のパスがcategories/backendの場合はcategories/backend/pages/ページ番号に遷移ができるようになります。

カテゴリページでのページネーションコンポーネント呼び出し時に現在のパスを渡すよう修正します。

import ArticleList from "@/components/article-list";
import CategoryFilter from "@/components/category-filter";
import Pagination from "@/components/pagination";
import { getArticles } from "@/lib/client";
import { LIMIT } from "@/lib/constants";

export default async function Page(props: {
  params: Promise<{ categoryId: string }>;
}) {
  const { categoryId } = await props.params;
  const { contents: articles, totalCount } = await getArticles({
    limit: LIMIT,
    offset: 0,
    filters: `category[equals]${categoryId}`,
  });

  return (
    <div>
      <CategoryFilter currentCategoryId={categoryId} />
      <ArticleList articles={articles} />
      <Pagination
        totalCount={totalCount}
        basePath={`/categories/${categoryId}`} {/* 追加 */}
      />
    </div>
  );
}

categories/[categoryId]フォルダ内にpages/[currentPage]フォルダを作成し、以下の内容でpage.tsxファイルを作成します。

import ArticleList from "@/components/article-list";
import CategoryFilter from "@/components/category-filter";
import Pagination from "@/components/pagination";
import { getArticles } from "@/lib/client";
import { LIMIT } from "@/lib/constants";

export default async function Page(props: {
  params: Promise<{ categoryId: string }>;
}) {
  const { categoryId } = await props.params;
  const { contents: articles, totalCount } = await getArticles({
    limit: LIMIT,
    offset: 0,
    filters: `category[equals]${categoryId}`,
  });

  return (
    <div>
      <CategoryFilter currentCategoryId={categoryId} />
      <ArticleList articles={articles} />
      <Pagination
        totalCount={totalCount}
        basePath={`/categories/${categoryId}`}
      />
    </div>
  );
}

カテゴリページでページネーションをクリックして各ページに遷移できるようになっていることを確認できれば完了です。

1ページ目

2ページ目

検索機能を作成

検索フォームコンポーネントの切り出し

最後に検索機能を作成します。

ヘッダーコンポーネントの検索フォームの箇所をコンポーネントに切り出します。

import Link from "next/link";
import Search from "./search"; // 追加

export default function Header() {
  return (
    <header className="border-b">
      <div className="container mx-auto flex items-center justify-between px-4 py-6">
        <Link href="/" className="text-2xl font-bold">
          Tech Blog
        </Link>
        <Search /> {/* 修正 */}
      </div>
    </header>
  );
}

検索フォームコンポーネントを以下の内容で作成します。

"use client";

import { FormEvent } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { useRouter, useSearchParams } from "next/navigation";

export default function Search() {
  const router = useRouter();
  const searchParams = useSearchParams();

  const createQueryString = (q: string) => {
    const params = new URLSearchParams(searchParams.toString());
    params.set("q", q);
    return params.toString();
  };

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    router.push("/search?" + createQueryString(e.currentTarget.q.value));
  };

  return (
    <form onSubmit={handleSubmit} className="mx-4 max-w-sm flex-1">
      <div className="flex w-full max-w-sm items-center space-x-2">
        <Input
          type="text"
          id="q"
          name="q"
          placeholder="Search articles..."
          defaultValue={searchParams.get("q")?.toString()}
        />
        <Button type="submit">Search</Button>
      </div>
    </form>
  );
}

検索結果ページの作成

Searchボタンを押した時の遷移先である検索結果ページを作成します。

import ArticleList from "@/components/article-list";
import CategoryFilter from "@/components/category-filter";
import { getArticles } from "@/lib/client";

export default async function Page(props: {
  searchParams: Promise<{ q: string }>;
}) {
  const searchParams = await props.searchParams;

  const { contents: articles } = await getArticles({
    q: searchParams.q,
  });

  return (
    <div>
      <CategoryFilter />
      <ArticleList articles={articles} />
    </div>
  );
}

クエリパラメータの値でmicroCMSのqパラメータを使って検索を行っています。

適当なキーワードで検索を行って、正常に絞り込みができれば検索機能は完了です。

これでブログの主な機能は完成しました。

まだSEO的に不十分な箇所もあるので別の記事で紹介しようと思います。

参考

https://blog.microcms.io/search-store/

https://help.microcms.io/ja/knowledge/add-search-to-site

share on
xアイコンfacebookアイコンlineアイコン