LoginSignup
63
12

Next.js × microCMSでMarkdownの入稿環境を整えてみよう!

Last updated at Posted at 2023-12-22

はじめに

こんにちは!

:santa:DMM WEBCAMP Advent Calendar 2023 :christmas_tree:23日目を担当する、 @Keichan_15 です!

普段はメンターとして受講生さんの学習サポートをさせて頂いています!
かれこれ2年目に突入しました:v:

去年に引き続き、今年もありがたいことにカレンダー執筆の機会を頂きました。
今日から私の方で3日間連続で公開予定ですので、お楽しみに!:relaxed:

2発目は何書くの!

さて、今年2発目の記事は 【Next.js × microCMSでMarkdownの入稿環境を整える】 です!
実はこちらの記事、昨日(22日目)を担当した @takakou の続きの記事となっております!

僕も実はmicroCMSを勧められて…使ってみたら本当に便利でビックリ…(まだレイアウトが完成しきれず公開できていませんが…)

一応途中経過はこんな感じ… ↓ 結構良さげじゃない?:thinking:

image.png

ただそんな便利なmicroCMSにも1つだけ難点がありました。
それはMarkdown専用のエディターが用意されていないんです!:crying_cat_face:

これ僕にとってはめちゃくちゃ致命的で、技術記事のMarkdownをそのまま移植できるようにしたかったんですよね。

ただデフォルトだとmicroCMSはリッチテキストエディタになるので、単純コピペでは反映されないという大大大 × 100欠点がありました。

image.png

悲しい…:disappointed_relieved:

今回はそんな問題を解決する為に、microCMSの拡張フィールドを使用してMarkdownでの入稿環境を構築してみたいと思います!

早速やっていきましょ~~!

環境

  • Next.js(v13.5.4)
  • Typescript5.0
  • React18
  • microCMS

環境自体は22日目の記事で構築したものをそのまま使っています!
ということは22日目を見ないと分からないってことだゾ!

普段はDockerで構築していますが、今回の記事用に別で用意するのが面倒だったのでWSL2を使いました。

Dockerでブログ環境を構築してえ…って方はこちらの記事見てサクッと構築するのもアリですね! (宣伝)

導入準備

今回の導入方法はmicroCMSの公式ブログを参考にさせて頂いています。

それでは早速Markdownの入力フォームを導入していきましょう。

イメージとしてはNext.js側でMarkdownの入力フォームを作成し、そこで入力したデータをmicroCMS側で形式を崩さず保存するといった方法になります。

まずは必要なライブラリをインストールしていきます。

terminal
$ npm install next-remove-imports @uiw/react-md-editor

次にnext.config.jsを以下のように編集します。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
};

const removeImports = require("next-remove-imports")();

module.exports = removeImports({
  ...nextConfig,
});

client.tsも併せて修正します。

libs/client.ts
import { createClient } from 'microcms-js-sdk';

export type Blog = {
    id: string;
    title: string;
+   content: string;   
}

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

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

export const client = createClient({
    serviceDomain: process.env.SERVICE_DOMAIN,
    apiKey: process.env.API_KEY,
});

// ブログ一覧を取得
export const getBlogs = async () => {
    const blogs = await client.getList<Blog>({
    endpoint: "blogs"
    });
    return blogs;
}

// ブログの詳細を取得
export const getDetail = async (contentId: string) => {
    const blog = await client.getListDetail<Blog>({
        endpoint: "blogs",
        contentId,
    });
    return blog;
};

最後にmicroCMSの拡張フィールドを利用するため、microcms-field-extension-reactもインストールしておきます。

terminal
$ npm install microcms-field-extension-react

これで導入準備が完了しました!便利だ~~:raised_hands:

Markdown Editor 実装

次にMarkdown Editorの表示部を実装していきます!

今回は表示部用のページとして、articles/new/page.tsxを作成し、そちらにMarkdown Editorを表示させるようにします。

articles/new/page.tsx
"use client";

import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { useFieldExtension } from "microcms-field-extension-react";
import "@uiw/react-md-editor/markdown-editor.css";
import "@uiw/react-markdown-preview/markdown.css";

// dynamic importを使用してMarkdown Editor for Reactを読み込む
const MDEditor = dynamic(() => import("@uiw/react-md-editor"), {
  ssr: false,
  loading: () => <div>initializing...</div>,
});

// 自身が利用しているmicroCMSのURLを設定
const origin = "https://hogehoge.microcms.io";

const IndexPage = () => {
  const [markdown, setMarkdown] = useState<string | undefined>();
  // microCMSのフィールド拡張を利用するためのhook
  const { data, sendMessage } = useFieldExtension("", {
    origin,
    height: 540,
  });

  useEffect(() => {
    if (!markdown) {
      setMarkdown(data);
    }
  }, [data, markdown]);

  return (
    <div data-color-mode="light">
      <MDEditor
        value={markdown}
        onChange={(value) => {
          setMarkdown(value);
          sendMessage({
            data: value,
          });
        }}
        height={540}
        textareaProps={{
          placeholder: "Please enter Markdown text",
        }}
      />
    </div>
  );
};

export default IndexPage;

実装が完了したら、microCMSを設定していきます。
microCMSにアクセスし、APIスキーマを開きましょう。

今回分かりやすいように、title以外のスキーマは削除しました。

image.png

フィールドを追加をクリック。

image.png

各種設定は以下のように入力しました。

  • フィールドID: content
  • 表示名: 内容

次に「種類」から拡張フィールドを選択します。

image.png

読み込み先URLの入力が求められるため、先ほどMarkdown Editorを設定したページのURLを貼り付けます。

今回であればhttp://localhost:3000/articles/newが読み込み先URLとなります。

image.png

URLの入力が完了したら、変更するをクリックして保存しましょう!

追加をクリックし新規ブログのページに遷移後、Markdown Editorが表示されていればOKです!

image.png

仮のブログデータを作成し、公開をクリックします。

image.png

テスターで試しに叩いてみると、しっかりとMarkdown Editorに入力された内容が取得できていることを確認できました!

image.png

Markdownを表示させていく!

Markdownの入稿環境が整ったので、次は入力したMarkdownをNext.js側で表示させていこうと思います!

ブログの表示ページはblogs/[blogId]/page.tsxにしました!
ぶっちゃけどのページでも構いません!ご自身の表示させたい所で実装してください。

まず以下のライブラリ類をインストールしていきます。

  • zenn-markdown-html
    MarkdownのコンテンツをHTMLに変換

terminal
$ npm i zenn-markdown-html
  • cheerio
    jQueryチックな書き方できるやつ、下に紹介のhighlight.jsを使用する為に導入

terminal
$ npm i cheerio
  • highlight.js
    シンタックスハイライト適用。コードに色が付くよ!

terminal
$ npm i highlight.js

ここから好きなパレットを選べます。

今回はnight-owl.cssを使用します!この辺りは好みの問題ですね:thinking:

それではブログの詳細画面を実装していきましょう!

blogs/[blogId]/page.tsx
import { notFound } from "next/navigation";
import { getDetail, getBlogs } from "@/../libs/client";
import markdownHtml from "zenn-markdown-html";
import { load } from "cheerio";
import hljs from "highlight.js";
import "highlight.js/styles/night-owl.css";

export async function generateStaticParams() {
  const { contents } = await getBlogs();
  console.log(contents);

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

export default async function StaticDetailPage({
  params: { blogId },
}: {
  params: { blogId: string };
}) {

  // ブログデータを取得
  const post = await getDetail(blogId);

  if (!post) {
    notFound();
  }

  // Markdown部分をHTMLに変換
  let html = markdownHtml(post.content);

  const $ = load(html);

  $("pre code").each((_, elm) => {
    const result = hljs.highlightAuto($(elm).text());
    $(elm).html(result.value);
    $(elm).addClass("hljs");
  });
  html = $.html();

  return (
    <div className="lg:col-span-2">
      <div className="flex flex-wrap w-full mb-20 flex-col items-center text-center">
        <h1 className="sm:text-3xl text-2xl font-medium title-font mb-2 text-gray-900">
          hoge hoge Tech Blog
        </h1>
        <p className="lg:w-1/2 w-full leading-relaxed text-gray-500">
          Qiita Test
        </p>
      </div>
      <div
        className="markdown"
        dangerouslySetInnerHTML={{ __html: html }}
      ></div>
    </div>
  );
}

実装自体は意外とすんなり書けました。:writing_hand:
基本的にこちらをこのままコピペで問題無いです!

次はCSSを当てていきましょう!

今回は採用しませんでしたが、特にこだわりが強い訳では無い場合、zenn-content-cssを使うのもアリです(CSSをZennに寄せたければ一番手っ取り早い)

今回はhighlight.jsを使用しているので、出来るところは自分でゴリゴリ書いています。今回ある程度の部分は @takakou の技術ブログのCSSをパクってきましたw(本人了承済)

ほんまにキツイCSS

globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

div.code-block-container {
  margin-top: 20px;
}

span.code-block-filename {
  color: #2c3e50; /* Matching text color */
  background-color: #ecf0f1;
  padding: 0.25rem 0.5rem;
  border-bottom-right-radius: 5px;
  font-size: 0.75rem;
  font-weight: bold;
  position: absolute;
}

.markdown h1 {
  margin-top: 50px;
  font-size: 1.9rem;
  font-weight: bold;
  margin-bottom: 3rem;
  color: #2c3e50; /* Maintaining the sophisticated look */
}

.markdown h2 {
  margin-top: 30px;
  font-size: 1.5rem;
  font-weight: bold;
  margin-top: 2rem;
  margin-bottom: 2rem;
  border-left: 3px solid #323232; /* Vibrant blue border */
  padding-left: 0.5rem;
  color: #2c3e50; /* Matching color */
  background-color: #ffffff; /* Subtle grey background */
}

.markdown h3 {
  font-weight: bold;
  text-decoration: underline;
  margin-top: 20px;
  font-size: 1.3rem;
  margin-top: 1.5rem;
  margin-bottom: 0.5rem;
  color: #2c3e50; /* Matching color */
}

.markdown h4 {
  font-weight: bold;
  font-size: 1.1rem;
  margin-top: 1.2rem;
  margin-bottom: 0.3rem;
  color: #2c3e50; /* Matching color */
}

.markdown ul {
  list-style-type: disc;
  margin-left: 2rem;
  padding-left: 10px;
}

.markdown li {
  font-weight: bold;
  padding: 3px;
}

.markdown ol {
  list-style-type: decimal;
  margin-left: 2rem;
}

.markdown a {
  color: #3498db; /* Blue link color */
  text-decoration: none;
  transition: color 0.2s;
}

.markdown a:hover {
  color: #2980b9; /* Slightly darker blue on hover */
  text-decoration: underline;
}

.markdown pre {
  background-color: #2c3e50; /* Darker grey background */
  padding: 1rem;
  padding-top: 1rem;
  padding-bottom: 0.1rem;
  border-radius: 5px;
  overflow-x: auto;
  color: #ecf0f1; /* Light text color */
  margin-bottom: 30px;
  /* margin-top: 30px; */
}

.markdown pre > code {
  background-color: #2c3e50;
  padding: 0.2rem 0.4rem;
  border-radius: 4px;
  font-family: "Fira Code", monospace; /* Monospace font for code */
  color: #ecf0f1; /* Light text color */
}

.markdown div {
  position: relative;
}

.markdown code {
  background-color: #eaecee;
  padding: 0.2rem 0.4rem;
  border-radius: 4px;
  font-family: "Fira Code", monospace; /* Monospace font for code */
  color: #c0392b; /* Red text color */
}

.markdown div div[data-filename]::before {
  /* content: attr(data-filename);
  color: #2c3e50; 
  background-color: #ecf0f1; 
  padding: 0.25rem 0.5rem;
  border-top-left-radius: 5px;
  border-bottom-right-radius: 5px;
  font-size: 0.75rem;
  font-weight: bold;
  position: absolute; */
}

.markdown blockquote {
  border-left: 5px solid #3498db; /* Matching blue border */
  padding-left: 1rem;
  margin: 10px;
  margin-left: 2rem;
  color: #7f8c8d; /* Slightly darker grey text color */
  font-style: italic;
}

.markdown hr {
  border: none;
  border-top: 2px solid #ecf0f1; /* Light grey border */
  margin: 1.5rem 0;
}

.markdown img {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 1.5rem auto;
  border-radius: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Light shadow */
}

.markdown p {
  padding-left: 10px;
}

@media (max-width: 768px) {
  .markdown h1 {
    font-size: 1.3rem;
  }

  .markdown h2 {
    font-size: 1.1rem;
  }

  .markdown h3 {
    font-size: 0.9rem;
  }

  .markdown h4 {
    font-size: 0.8rem;
  }

  .markdown ul {
    margin-left: 1rem;
  }

  .markdown ol {
    margin-left: 1rem;
  }

  .markdown pre {
    font-size: 0.9rem;
  }

  .markdown code {
    font-size: 0.9rem;
  }

  .markdown blockquote {
    font-size: 1rem;
  }
}

.timeline {
  list-style: none;
}
.timeline > li {
  margin-bottom: 60px;
}

/* for Desktop */
@media (min-width: 640px) {
  .timeline > li {
    overflow: hidden;
    margin: 0;
    position: relative;
  }
  .timeline-date {
    width: 110px;
    float: left;
    margin-top: 20px;
  }
  .timeline-content {
    width: 75%;
    float: left;
    border-left: 3px #e5e5d1 solid;
    padding-left: 30px;
    padding-top: 20px;
  }
  .timeline-content:before {
    content: "";
    width: 12px;
    height: 12px;
    background: #837ccf;
    position: absolute;
    left: 106px;
    top: 24px;
    border-radius: 100%;
  }
}

実装が完了したら、http://localhost:3000/blogs/(コンテンツID) を入力して画面に遷移してみましょう!

コンテンツIDの確認方法はシンプルで、microCMSにアクセスして先ほど作成したブログをクリックすると以下の詳細画面が開きます。

image.png

左上にコンテンツIDが記載されています。こちらをURLに設定してください。
画像で言うところのdq2wc7jttになります。

image.png

画面が開き、以下のようなページが表示されていればOKです!お疲れ様でした!:v:

image.png

ちなみにコードブロックでコードを書いていた場合、表示は以下のようになります。

image.png

ファイル名表示にも対応させています。これ何かとしんどかったです(笑)

image.png

めちゃくちゃ良い感じじゃない??:relaxed:

色合い等々に関しては、CSSをある程度触ったことのある方でしたら globals.css を弄ることで自由自在に変更できるので、是非ご自身のお好きなレイアウトにしてみてくださいね!

ちなみに今回は時間の都合上書きませんでしたが、Cloudflare R2を使用すれば画像をドラッグ & ドロップで投稿できるようにもなります!
これは僕が元気であれば年明けに書くかもしれません、あまり期待しないで!

皆さんも是非世界に1つだけのブログを作ってみましょう!!

参考

おわりに

いかがでしたでしょうか。

今回はNext.jsとmicroCMSを使用して、Markdownの入稿環境を構築してみました!
Qiitaの記事もそうですが、Markdownって1回書いたらもうMarkdown以外書けなくなりません!?ってくらい良いので、是非皆さんも実践してみて頂けると幸いです!:bow:

明日24日目も私が担当させて頂きました!
原点に戻りRailsにまつわる記事を書かせて頂きましたので、良ければ見てくださると泣いて喜びます!:joy:

最後までご覧頂き、ありがとうございました!!

63
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
63
12