Next.jsマークダウン記事のデザイン適用

投稿 2021.10.12

/更新 2021.10.15

目次

この記事では npm プラグインである「markdown-it」とCSSを用いて、Next.js製のブログの記事ページをイケてるデザインに変えていきます。

何回かNext.jsで作る無料ブログ構築の記事を書いてきました。

https://24365.dev/posts/20211011-nextjs-edit-01/

上記記事で構築したNext.jsプロジェクトのソースコードに対して適用していく想定で書いていきます。

「markdown-it」を使いマークダウン機能を拡張

後々拡張していくのに便利なので「markdown-it」というプラグインを入れます。

https://github.com/markdown-it/markdown-it

コマンド実行
npm install markdown-it --save 
src/utils/Markdown.ts
-// TODO: @mapbox/rehype-prism does not have typescript definition
 // @ts-ignore
-import rehypePrism from '@mapbox/rehype-prism';
-import html from 'rehype-stringify';
-import gfm from 'remark-gfm';
-import remarkParse from 'remark-parse';
-import remarkRehype from 'remark-rehype';
-import unified from 'unified';
+import MarkdownIt from 'markdown-it'
 
 export async function markdownToHtml(markdown: string) {
-  const result = await unified()
-    .use(remarkParse)
-    .use(gfm)
-    .use(remarkRehype)
-    .use(rehypePrism)
-    .use(html)
-    .process(markdown);
-  return result.toString().replace(//assets/images/posts/20211012-nextjs-markdown/g, process.env.baseUrl || '');
+  const markdownIt = new MarkdownIt(
+    {
+      langPrefix: 'lang-',
+      preset: 'default',
+      linkify: true,
+      breaks: true,
+      html: true,
+      typegraphy: true,
+    }
+  )
+  return markdownIt.render(content).replace(//assets/images/posts/20211012-nextjs-markdown/g, process.env.baseUrl || '');
 }

langPrefix:コードブロックのCSSクラス名の接頭辞に、指定した文字列を付加します。
preset:パースするファイルの規格(Markdown/CommonMark)を指定します。
linkify:「https://github.com」こんな感じのURL文字列をリンクにしてくれます。
breaks:
タグで改行してくれます。
html:htmlタグをhtmlタグとして読み込んでくれます。
typegraphy:言語に依存していない引用符などを綺麗にします

「markdown-it-container」をインストール

「markdown-it-container」では以下のようにマークダウン上で記述した部分を

:::test
  こんにちは
:::

以下のような形に置き換えることができます

<div class="test">
  こんにちは
</div>

これの何が嬉しいかというと、定型のよく使うデザインがあった場合、「test」というcssクラスにデザインを適用しておけば簡単に統一されたデザインをマークダウン上で使うことができます。

npmでインストールしていきます。

コマンド実行
npm install markdown-it-container --save

先ほど編集したばかりのファイルにさらに追記していきます。

src/utils/Markdown.ts
 // @ts-ignore
 import MarkdownIt from 'markdown-it'
+// @ts-ignore
+import mdContainer from 'markdown-it-container'
 
export async function markdownToHtml(content: string) {
  const markdownIt = new MarkdownIt(
    {
      langPrefix: 'language-',
      preset: 'default',
      linkify: true,
      breaks: true,
      html: true,
      typegraphy: true,
    }
+  ).use(mdContainer, 'afilink', containerAfilinkOptions)
+  .use(mdContainer, 'question', containerQuestionOptions)
+  .use(mdContainer, 'attention', containerAttentionOptions);
+
+  return markdownIt.render(content).replace(//assets/images/posts/20211012-nextjs-markdown/g, process.env.baseUrl || '');
 }
 
+// ::: afilink
+//   text
+// :::
+var afiClassRegex = /^afilink$/;
+const containerAfilinkOptions = {
+    validate: function (params:any) {
+        return afiClassRegex.test(params.trim());
+    },
+    render: function (tokens:any, idx:number) {
+        if (tokens[idx].nesting === 1) {
+            // opening tag
+            return '<div class="afi">';
+        }
+        else {
+            // closing tag
+            return '</div>\n';
+        }
+    },
+};
 
+// ::: question
+//   text
+// :::
+var queClassRegex = /^question$/;
+const containerQuestionOptions = {
+    validate: function (params:any) {
+        return queClassRegex.test(params.trim());
+    },
+    render: function (tokens:any, idx:number) {
+        if (tokens[idx].nesting === 1) {
+            // opening tag
+            return '<div class="que">';
+         }
+        else {
+            // closing tag
+            return '</div>\n';
         }
+    },
+};
+ 
+// ::: attention alert
+//   text
+// :::
+var utils_1 = require("markdown-it/lib/common/utils");
+var attClassRegex = /^attention\s*(alert)?$/;
+const containerAttentionOptions = {
+    validate: function (params:any) {
+        return attClassRegex.test(params.trim());
+    },
+    render: function (tokens:any, idx:number) {
+        var m = tokens[idx].info.trim().match(attClassRegex);
+        var attClassName = (m === null || m === void 0 ? void 0 : m[1]) || '';
+        if (tokens[idx].nesting === 1) {
+            // opening tag
+            return '<div class="att ' + utils_1.escapeHtml(attClassName) + '">';
+        }
+        else {
+            // closing tag
+            return '</div>\n';
+        }
+    },
+};

下の形式でマークダウンファイルに記述する想定でソースコードを書いています。
自分の用途に合わせて編集してください。


:::afilink
ここは主にアフィリエイトのリンクをボタン風にするデザインを適用します。
:::

:::question
ここは記事冒頭などに読者の疑問をまとめるのに使います。
:::

:::attention
ここは大事なことを強調したいときに使います。
:::

:::attention alert
ここは注意点などをまとめたいときに使います。
:::

githubのCSSを適用していく

自分でデザインを適用してもいいのですが、ベースのデザインには以下のgithubのcssを適用します。

https://github.com/sindresorhus/github-markdown-css

シンプルで無難な仕上がりになると思います。

Head部分でCDNを読み込みます。

src/pages/_document.tsx
   render() {
     return (
       <Html lang="jp">
-        <Head />
+        <Head>
+        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/github-markdown.min.css" />
+        </Head>
         <body>
           <Main />
           <NextScript />

記事部分に適用されるように「markdown-body」クラスを適用します。

src/pages/posts/[slug].tsx
     <div className="text-center text-sm mb-8">{format(new Date(props.date), 'LLLL d, yyyy')}</div>
 
     <Content>
-      <div
+      <div className="markdown-body"
         // eslint-disable-next-line react/no-danger
         dangerouslySetInnerHTML={{ __html: props.content }}
       />

ベースのデザインはこれでOKです。

オリジナルのデザインを適用していく

「src/pages/_app.tsx」で既に読み込まれている「src/styles/main.css」にcssを書いていきます。

src/pages/_app.tsx
import React, { useEffect } from 'react';

import { AppProps } from 'next/app';

import '../styles/main.css';
import '../styles/prism-a11y-dark.css';

import { GA_TRACKING_ID, pageview } from '../utils/Gtag';

なければcssファイルを作って、「src/pages/_app.tsx」で読み込んでください。
「.markdown-body」配下の要素にだけ適用するように思い思いに書いていきます。
「@apply」の書き方はtailwind cssをインストールしている場合に有効な書き方です。

インストールしていない場合は「.markdown-body .que」の書き方を参考に書いてみてください。

@tailwind components;

@tailwind utilities;
@layer components {
  .markdown-body{
    @apply bg-white md:p-8 p-4 shadow-md rounded-md border;
  }
  .markdown-body h1 {
    @apply mt-12;
  }
  .markdown-body h2 {
    @apply mt-10;
  }
  .markdown-body h3 {
    @apply bg-gray-200 mt-10 py-2;
  }
  .markdown-body p {
    @apply leading-loose;
  }
  .markdown-body table{
    @apply table w-full;
  }
  .markdown-body tr {
    @apply table-row;
  }
  .markdown-body th, .markdown-body td {
    @apply table-cell;
  }
  .markdown-body ul li, .markdown-body ol li {
    @apply relative -left-3 list-disc;
  }
  .markdown-body .afi a{
    @apply block border bg-blue-500 transition duration-300 font-semibold shadow-sm text-sm;
    @apply text-white text-center py-2 px-4 rounded-lg cursor-pointer;
    @apply hover:bg-blue-600 hover:no-underline;
  }
  .markdown-body .que {
    position:relative;
    margin:1.5rem 0;
    padding:21px 15px 21px 70px;
    border-radius:6px;
    background:#eff7ff;
    color:rgba(0,0,0,.65);
    font-size:.94em;
    line-height:1.6
  }
}

以下のブログに適用してみました。

https://kekke.dev

「markdown-it」は「markdown-it-container」のようなプラグインがたくさんあり、拡張性が高いです。
他にもプラグインが用意されているので使ってみてください。

効率よく独学でプログラミングを学びたい方へ

以下の記事を書いていますので参考にしてみてくださいね。

https://24365.dev/posts/20211007-programming-study/

以上、マークダウン記事のデザイン適用方法でした。

この記事を書いた人

avatar

にいよん

WEBエンジニアで0&2歳娘のパパです。フルリモートでちいたまに引きこもり中。密かにFIRE目指してます。こんな情報を発信中→投資、IT技術、副業、仮想通貨、暗号資産、NFT、メタバース、ブロックチェーン

© 2021 - 2022 24365.dev