microCMS + next.jsでブログ作成
こちらの内容を使って個人ブログを作成する。
現状のスキルレベル
- Next.jsを多少触っている
- 基本的にフロントエンドではなくコーディングのスキルがメイン
- APIを使ったり、機能開発をしたことはほぼ無し
モチベーション
フロントエンドの学習をしてスキルアップしていく
Github
APIキーの取得方法
API設定 > APIリファレンス > X-API-KEYから取得
こっちでもいけた
export const getStaticPaths = async() =>{
// dataにエンドポイントblogの情報を取得する
const data = await client.get({endpoint: "blog"});
// pathsにdataから取り出したidをURLとして代入する
const paths = data.contents.map((content) => `/blog/${content.id}`);
// fallbackをfalseにするとpathsで返したURL以外404に
return {paths, fallback: false};
}
getStaticPropsの引用
contextパラメータは次のキーを含むオブジェクトです:
paramsはページが動的ルートを利用するためのルートパラメータを持ちます。たとえば、ページ名が [id].js >である時、paramsは { id: ...} のように見えます。詳細は 動的ルーティングのドキュメントをご覧ください。>後に説明する getStaticPathsと一緒に使う必要があります。
ページがプレビューモードになっている時は preview が true になり、そうでない場合は false になります。>プレビューモードのドキュメントをご覧ください。
previewDataは、setPreviewDataによって設定されたプレビューデータを含みます。プレビューモードのドキュメント をご覧ください。
挙動的にはこれ
// 既にcontextというオブジェクトが存在している
export const getStaticProps = async(context) => {
// ページidをidに代入
const id = context.params.id;
// エンドポイント"blog"のページIDが"id"のdataを取得
const data = await client.get({endpoint:"blog", contentId: id});
return{
props:{
blog:data,
},
}
}
getStaticPropsが少し理解できてきた
.gitignoreに.nextを追加
スタイルは追加せずに一旦完了
約2hぐらいで完成。
日本語で丁寧に説明してあってとてもわかり易かった。
残りやりたいこと
- スタイル追加
Typescriptに変更- ページネーション追加
プレビュー環境を整えるtailwind追加yarnに統一
{ props: posts } を返すことで、Blog コンポーネントは
ビルド時にposts
を prop として受け取ります。
ここで受け取った値はpropとして使用できるようになる
Tailwindの導入
tailwind公式
パッケージ導入
yarn add tailwindcss@latest postcss@latest autoprefixer@latest
この記事はわかりやすかった
Typescriptの導入
公式
-
tsconfig.jsonを作成
-
パッケージ導入
yarn add --dev typescript @types/react @types/node
- yarn buildをすると初期設定が入る
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
-
.jsを.tsxにリネーム
-
とりあえずエラー削除していく
anyでお茶を濁したけど合っているかわからいない
export const getStaticPaths: GetStaticPaths = async () => {
const data: any = await client.get({ endpoint: "blog" });
const paths = data.contents.map((content) => `/blog/${content.id}`);
console.log(data)
return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps = async (context) => {
const id: any = context.params.id;
const data = await client.get({ endpoint: "blog", contentId: id});
return {
props: {
blog: data,
},
};
};
export const getStaticProps: GetStaticProps = async () => {
const data: any = await client.get({ endpoint: "blog" });
return {
props: {
blog: data.contents,
},
};
};
プレビュー環境
エラーが発生
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (!req.query.id) {
return res.status(404).end();
}
const id = req.query.id;
const draftKey = req.query.draftkey;
const content = await fetch(
`https://webdock.microcms.io/api/v1/blog/${id}?fields=id&draftKey=${draftKey}`,
{ headers: { 'X-API-KEY': process.env.API_KEY || '' } }
)
.then(res => res.json()).catch(error => null);
if (!content) {
return res.status(401).json({ message: 'Invalid slug' });
}
res.setPreviewData({
slug: content.id,
draftKey: req.query.draftKey,
});
res.writeHead(307, { Location: `/${content.id}` });
res.end('Preview mode enabled');
};
検証
contentがundefineになっている
→ fetchがうまくいっていない
→ draftKeyの取得ができていない
→ タイポしてました。。。
これで一日オワタ
microCMSサイトの記述だとディレクトリがpage配下になってしまうので、/blog/[id].tsxにプレビューを表示させるように変更
res.writeHead(307, { Location: `/blog/${content.id}` });
プレビューが表示されるのは、page/[id].tsxのディレクトリなのでプレビュー専用のファイルを作成する。
page/blog/[id].tsxをコピペし、
pathsを書き換え
export const getStaticPaths: GetStaticPaths = async () => {
const data: any = await client.get({ endpoint: "blog" });
// /blog/${content.id}のblogを削除
const paths = data.contents.map((content) => `/${content.id}`);
return { paths, fallback: true };
};
記述かぶっているのでリファクタリングしたい。
ダイナミックルートで解決できそうかも。
プレビュー自体はできているので一旦後回し
draftKeyの型エラー
こちらの記事で修正できた。
APIの挙動も調べた↓
エラー
投稿側にきちんと全てデータを入れると通った
APIルート
next.jsはAPIを構築できる。
例では、pages/api/user.jsにアクセスしたときのresを定義している
APIルートを使うためには以下の関数をエクスポートする必要がある
export default (req, res) => {
};
resの中身
HTTPステータスコード
スタイルの追加
CSSの選定は、Next.jsで推奨されているTailwindとCSS Mosulesで記述することにする
ちょっと古いけれどめちゃくちゃ参考になった
Sassの追加
$ yarn add sass
scssファイルも使えます。
TOPページのスタイリング大体終了
ナビゲーションつけたいなー
day.jsの追加
公開日をフォーマットするためにday.jsを追加する
yarn add dayjs
LinkタグとImageタグを使う
Imageタグを使う
画像最適化のためにImageタグを使いたい。
module.exports = {
reactStrictMode: true,
// micro-cmsのドメインを追加
images: {
domains: ["images.microcms-assets.io"],
},
};
シンタックスハイライト追加
参考
yarn add highlight.js
yarn add cheerio
import cheerio from "cheerio";
import hljs from "highlight.js";
import "highlight.js/styles/github-dark.css";
export default function Blogid({ content, highlightedBody }: contentProps) {
return (
<>
<Header />
<main className={blogStyles.post}>
<div className="max-w-3xl p-11 mx-auto bg-white rounded-xl shadow-md">
<h1>{content.title}</h1>
<div className="mt-2">
<span className="text-gray-600">
{dayjs(content.publishedAt).format("YYYY.MM.DD")}
</span>
<span className="ml-5 text-sm text-gray-600 underline">
{content.category && `${content.category.name}`}
</span>
</div>
<div className="mt-12">
{content.eyecatch ? (
<Image
src={content.eyecatch.url}
width={content.eyecatch.width}
height={content.eyecatch.height}
/>
) : (
<Image src="/noimage.png" alt="No Image" />
)}
</div>
<div dangerouslySetInnerHTML={{ __html: highlightedBody }}></div>
</div>
</main>
</>
);
}
export const getStaticProps: GetStaticProps = async ({
params,
previewData,
}) => {
const id = params?.id;
const isDraft = (item: any): item is { draftKey: string } =>
!!(item?.draftKey && typeof item.draftKey === "string");
const draftKey = isDraft(previewData);
const content = await fetch(
`https://webdock.microcms.io/api/v1/blog/${id}${
draftKey !== undefined ? `?draftKey=${draftKey}` : ""
}`,
{ headers: { "X-API-KEY": process.env.API_KEY || "" } }
).then((res) => res.json());
const $ = cheerio.load(content.body);
$("pre code").each((_, elm) => {
const result = hljs.highlightAuto($(elm).text());
$(elm).html(result.value);
$(elm).addClass("hljs");
});
return {
props: {
content,
highlightedBody:$.html()
},
};
};
めっちゃいいのを見つけてしまった。
これ使うと勉強にならなさそうだから使うの迷う
Google Analyticsの導入
Scriptは直接_app.tsxに書く
import "../styles/global.scss";
import "tailwindcss/tailwind.css";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import { existsGaId, pageview, GA_ID } from "../libs/gtag";
import Script from "next/script";
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
if (!existsGaId) {
return;
}
const handleRouteChange = (path) => {
pageview(path);
};
router.events.on("routeChangeComplete", handleRouteChange);
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
};
}, [router.events]);
return (
<>
{GA_ID !== undefined && (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}
onLoad={() => {
const script = document.createElement("script");
script.innerHTML = `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_ID}', {
page_path: window.location.pathname,
});`;
document.body.appendChild(script);
}}
/>
</>
)}
<Script
strategy="beforeInteractive"
src="https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.2/lazysizes.min.js"
integrity="sha512-q583ppKrCRc7N5O0n2nzUiJ+suUv7Et1JGels4bXOaMFQcamPk9HjdUknZuuFjBNs7tsMuadge5k9RzdmO+1GQ=="
crossOrigin="anonymous"
/>
<Component {...pageProps} />
</>
);
}
export default MyApp;
OGPの追加
以下のブログを参考にそのまま記載
import Head from "next/head";
interface MetaData {
pageTitle?: string;
pageDescription?: string;
pagePath?: string;
pageImg?: string;
pageImgWidth?: number;
pageImgHeight?: number;
}
export default function Seo({
pageTitle,
pageDescription,
pagePath,
pageImg,
pageImgWidth,
pageImgHeight,
}: MetaData) {
const defaultTitle = "Hiroki Kameda's Blog";
const defaultDescription = "demo";
const title = pageTitle ? `${pageTitle} | ${defaultTitle}` : defaultTitle;
const description = pageDescription ? pageDescription : defaultDescription;
const url = pagePath;
const imgUrl = pageImg;
const imgWidth = pageImgWidth ? pageImgWidth : 1280;
const imgHeight = pageImgHeight ? pageImgHeight : 640;
return (
<Head>
<title>{title}</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:title" content={title} />
<meta property="og:site_name" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:image" content={imgUrl} />
<meta property="og:image:width" content={String(imgWidth)} />
<meta property="og:image:height" content={String(imgHeight)} />
<meta name="twitter:card" content="summary_large_image" />
<link rel="canonical" href={url} />
</Head>
);
}
ブログURLを追加
export default function Blogid({ content, highlightedBody, toc }: BlogProps) {
const router = useRouter();
const pagePath = `https://micro-cms-blog-nu.vercel.app${router.asPath}`;
return (
<>
<Seo
pageTitle={content.title}
pagePath={pagePath}
pageDescription={content.description}
pageImg={content.eyecatch.url}
pageImgHeight={content.eyecatch.height}
pageImgWidth={content.eyecatch.width}
/>
<Header />
//省略
ファビコンを用意