【Nextjs15】microCMSとAIを使って技術ブログを作成する 後編
前回は静的にブログページを作成、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
という配列で返されるため、contents
をcategories
という変数で受け取るようにしています。
起動して次のように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的に不十分な箇所もあるので別の記事で紹介しようと思います。