rials6.1でmarkdownを使用したミニブログアプリを作ってみる

# 1. 開発環境
* M1 Mac Monterey 12.0.1
* ruby3.0
* rails6.1.4.1

githubはこちらです。

https://github.com/taiki-nd/markdown-blog

オリジナルアプリにActionTextかMarkdownかで迷ったのでミニアプリを作って比べてみることにしました。

https://qiita.com/taiki-nd/items/556b804fc367ba30ca15

# 2. 実装開始

## 2.1 アプリ作成、初期設定

まずは新しいアプリの作成をしていきます。
``rails _6.1.4.1_ new markdown-blog -d mysql``

database.ymlの編集をします。

```ruby:config/database.yml
default: &default
  adapter: mysql2
  # encoding: utf8mb4
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock
```

## 2.2 turbolinksの無効化

以下の2つのファイルを編集、修正していきます。

```app/javascript/packs/application.js
import Rails from "@rails/ujs"
// import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"

Rails.start()
Turbolinks.start()
ActiveStorage.start()
```

```erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>MarkdownBlog</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <!-- 'data-turbolinks-track': 'reload'を削除 -->
    <%= stylesheet_link_tag 'application', media: 'all' %>
    <%= javascript_pack_tag 'application' %>
    <!-- ここまで -->

  </head>

  <body>
    <%= yield %>
  </body>
</html>
```

## 2.3 必要なもののインストール

まずは追記をします。

```:gemfile
gem 'image_processing', '~> 1.2'
```

続いてターミナルで以下を実行していきます。

``bundle install``
``rails db:create``
``rails active_storage:install``
``rails db:migrate``
``yarn add simplemde``
``yarn add inline-attachment``

ここでは

* ActiveStorage(ファイルアップロードの機能)
* SimpleMDE(ブラウザ上のtextareaタグをMarkdownエディタを変換するOSS)
* Inline Attachment(ブラウザ上からドラッグ&ドロップでのAjaxでのファイル送信を簡単に実装)

を導入しています。

## 2.4 model, controller, viewの編集
scaffoldでサクッと作成していきます。
``rails g scaffold post content:text``
``rails db:migrate``

続いて以下のファイルを編集します。

```erb:app/views/posts/_form.html.erb
<!-- 追記（画面で読み込むjsを読み込む） -->
<%= javascript_pack_tag 'post' %>

<%= form_with(model: post) do |form| %>
  <% if post.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
        <% post.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :content %>
    <!-- 編集 -->
    <%= form.text_area :content, { id: "markdown-editar"} %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
```

続いてapp/javascript/packs配下にpost.jsを作成します。
そして以下のように記述していきます。ここではSimpleMDEの導入と、ドラッグ&ドロップの処理を記述しています。

```javascript:app/javascript/packs/post.js
//インストールした機能をimportする
import "inline-attachment/src/inline-attachment";
import "inline-attachment/src/codemirror-4.inline-attachment";
import 'simplemde/dist/simplemde.min.css'
import SimpleMDE from "simplemde";
import Rails from '@rails/ujs'

window.onload = function () {
  // textareaをMarkdownエディタにする
  const simplemde = new SimpleMDE({
    element: document.getElementById("markdown-editar"),
  });
  // エディタに画像がドラッグ&ドロップされた際の処理
  inlineAttachment.editors.codemirror4.attach(simplemde.codemirror, {
    uploadUrl: "/posts/attach", // POSTする宛先Url
    uploadFieldName: "image", // ファイルのフィールド名(paramsで取り出す時のkey)
    allowedTypes: ['image/jpeg', 'image/png', 'image/jpg', 'image/gif'],
    extraHeaders: { "X-CSRF-Token": Rails.csrfToken() }, // セキュリティ対策
  });
};
```

## 2.5 ajaxの処理
``rails g model attachment``
で以下のように編集します。

```ruby:app/model/attachment.rb
class Attachment < ApplicationRecord
  has_one_attached :image
end
```

続いてはコントローラーの処理です。ここでjson形式にして値を返します。

```ruby:app/controller/posts_controller.rb
class PostsController < ApplicationController
  before_action :set_post, only: %i[ show edit update destroy ]

  # GET /posts or /posts.json
  def index
    @posts = Post.all
  end

  # 略

  # Ajax通信の記述を追記
  def attach
    attachment = Attachment.create! image: params[:image]
    render json: { filename: url_for(attachment.image) }
  end
　　　　# ここまで

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def post_params
      params.require(:post).permit(:content)
    end
end
```

最後に以下を追記してください。

```ruby:config/route.rb
post 'posts/attach', to: 'posts#attach'
```

これで画像の処理ができました。しかし、これではmarkdownがhtmlに変換されずに出力されてしまいますので直していきます。
まずは現状確認からしていきましょう。

## 2.6 redcarpetの導入
現状確認

ドラッグ＆ドロップしてみてください。
![スクリーンショット 2021-11-15 0.36.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1645779/d0ab8918-242a-00e6-08fa-b54a08687072.png)

うまくアップロードできていますが、create postをクリックすると...
![スクリーンショット 2021-11-15 0.38.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1645779/643f4fea-f5d8-5d33-767a-d818c674bdf2.png)

これはまずいですね。そのまま表示されています。
これからmarkdownがhtmlに変換していきます。gemで解決します。

以下を追記してください。

```:gemfile
gem 'redcarpet'
```

``bundle install``

これでひとまず導入が完了です。
続いて``app/helper``に``markdown_helper.rb``ファイルを作成します。さらに以下のように記述していきます。

```ruby:app/helper/markdown_helper.rb
module MarkdownHelper
  def markdown(text)
    unless @markdown
      options = {
        filter_html: true,
        autolink: true,
        space_after_headers: true,
        no_intra_emphasis: true,
        fenced_code_blocks: true,
        tables: true,
        hard_wrap: true,
        xhtml: true,
        lax_html_blocks: true,
        strikethrough: true
      }
      renderer = Redcarpet::Render::HTML.new(options)
      @markdown = Redcarpet::Markdown.new(renderer)
    end

    @markdown.render(text).html_safe
  end
end
```

さらに詳細ページを編集していきます。

```erb:app/views/posts/show.html.erb
<p id="notice"><%= notice %></p>

<p>
  <strong>Content:</strong>
  <!-- ここを編集 -->
  <%= markdown(@post.content) %>
</p>

<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
```

これでアップした画像が表示されるはずです。
![スクリーンショット 2021-11-15 0.50.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1645779/2359fae8-cebf-51c1-84f4-0e174176c08f.png)

これで完了です！