振り返ると良い1年だった

2025年は熱を出して寝込む回数が多かったり、自宅の鍵紛失未遂だったり、人見知りを拗らせまくったり、思い出すと色々あったのですが、それでも1年間たくさんの人と関われて、たくさんコードを書いて、いろいろな場所に出かけて・・・振り返ると充実した良い1年だったなと思います。

今年も1年、ありがとうございました!

トピックごとにざーっと振り返りをしようと思います。

カンファレンス参加

気がついたらたくさんのカンファレンスに参加していました。 2026年はどうなるかな。

  • 東京Ruby会議 12
  • TokyoWomen.rb
  • RubyKaigi 2025
  • 関西Ruby会議 08
  • PicoRuby overflow
  • Kaigi on Rails 2025
  • VimConf small 2025
  • TokyuRuby会議 16
  • 北陸Ruby会議01

しんめ.rb

途中お休み期間をいただきましたが、12月に通算60回目まで開催を続けることができました。今年もたくさんの方が参加してくださり、ありがとうございました! 8月から開催を再開したのですが、開催頻度を減らしたのと Discord や esa を使うなどツールを見直してみたのが結果的には良かったです。

地域 Ruby 会議などに参加すると、しんめに参加してくださっている方と直接会える機会も多く、自分がせっせとカンファレンスに行く動機の一つになっていたりします。

登壇

TokyoWomen.rb で「Rails 1.0 のコードで学ぶ method_missing と find_by_* 」というタイトルで発表しました。 あと、LTだと TokyuRuby会議 16 で「Gentoo 1年生 ビルドは終わらない」というタイトルで発表しました。

Gentoo Linux を使い始める

2025年の一番大きな変化はこれだと思う。 Gentoo Linux を使い始めて、Web サービスを作るために RubyRails を学んで成長したい!に1点集中だった自分の気持ちに新しい領域が加わりました。 ここはまだふわふわしていて具体的に表現できないのですが、OS や CLI 周りへの興味がとても強くなっています。Linux デスクトップを使うようになって、ターミナルや Zsh の設定も色々試して自分はどう使っていきたいのかを前よりも考えるようになりました。

仕事

一番頑張った。まだまだ自信はないし未熟者ですが、2025年に頑張ることとして挙げた「Webサービスの開発に必要な基礎体力を鍛える」は周りの方々にたくさん助けていただきながら概ね達成できたと思います。

maimux2x.hatenablog.com

私はPM出身で、良い Web サービスとは?みたいな問いがずっと自分の中にあるので、来年もこの問いを自分なりに突き詰めていきたい。今年は技術を身につければ良いサービスが作れるという自分の頭の片隅にあった思い込みをかち割ることができたので、「何を作るのか・なぜ作るのか」という部分もバランスをとりながら考えられるようになっていきたいと思っています。

趣味

推しのライブにたくさんいきました!生のパフォーマンスを観ることが自分にとって一番元気をもらえてリフレッシュできる時間なので、来年も可能な範囲で足を運びたい。 あとは、相変わらず陶芸でお皿とかマグカップを作ったり、エンジニアに転職して以降やっていなかったパン作りとかお菓子作りを再開することができました。

終わりに

早いものでエンジニアにキャリアチェンジして丸2年が経っていました。フィヨルドブートキャンプに通っていた頃を含めると早4年近くプログラミングに触れているわけですが、そこで費やしてきた時間に対して今の自分は成長が鈍化してないか・・・?みたいな不安が今年は強く、それが何となく自分の外側にも出てしまった1年だったなぁと思っています。

振り返ってみると自分なりに登壇や新しいことにトライして、技術面での課題も逃げそうになりながらも何とか踏ん張って向き合えたんじゃないか?!と思います。

来年はそんな自分を認めてもう少し自信を持っていきたい。

2026年もよろしくお願いします!

私の好きな Ruby の変数代入 Tips

Ruby/Rails Advent Calendar 2025」の23日目の記事です!

qiita.com

先日、Ruby リリース30周年パーティに参加してきたのですが、歴代リリースマネージャの方の座談会、Ruby の未来の話など Ruby に関わるたくさんの方の話を伺うことができてとても充実した時間を過ごすことができました。

Ruby というプログラミング言語も、そのコミュニティも大好きな自分ですが、今回の記事では「私の好きな Ruby の変数代入 Tips」と題して、3つの方法に触れたいと思います。

partition で「意味のある2つ」に分ける

一つ目は Enumerable#partition メソッドです。

docs.ruby-lang.org

プログラムを書いていると、配列をある条件で2つに分けて扱いたくなることがよくありませんか。

partition は、ブロックで定義した条件の結果を2つの配列としてそのまま変数に受け取ることができる便利なメソッドです。

EC サイトのクーポンの有効期限を例に使い方を見てみたいと思います。

today = Date.new(2025, 12, 23) # Date.today だと日付が変動してしまうため、例として固定

coupons = [
  { code: 'COUPON_1', expires_on: Date.new(2025, 12, 10) },
  { code: 'COUPON_2', expires_on: Date.new(2025, 12, 25) },
  { code: 'COUPON_3', expires_on: Date.new(2025, 11, 15) },
  { code: 'COUPON_4', expires_on: Date.new(2026, 1, 1) }
]

active_coupons, expired_coupons = coupons.partition { it[:expires_on] >= today }

# expires_on は Date オブジェクトですが、分かりやすいように ISO 8601 形式で結果を載せています
pp active_coupons
# [{code: "COUPON_2", expires_on: "2025-12-25"},
# {code: "COUPON_4", expires_on: "2026-01-01"}]

pp expired_coupons
# [{code: "COUPON_1", expires_on: "2025-12-10"},
# {code: "COUPON_3", expires_on: "2025-11-15"}]

こんな感じで「何を基準に分けているのか」がコードから自然に読み取れるところが好きです。

values_at でハッシュや配列から必要な値だけを受け取る

2つ目は Hash#values_at です。

docs.ruby-lang.org

docs.ruby-lang.org

Rails の params から複数の値を取り出す際にとてもよく使います。

values_at は、引数で指定したキーに対応する値の配列を返します。そのため、分割代入を使って結果を個々の変数として受け取ることができます。

params = {variant: "りんご", origin: "青森県", producer: "Ruby 農園", price: 230}

variant, origin, price = params.values_at(:variant, :origin, :price)

variant #=> "りんご"
origin #=> "青森県"
price #=> 230

また、配列のインデックスを指定して値を取り出せるため、分割代入で不要な値を _ のように書かずに済みます。

row = ["001", "maimu", "maimu@example.com", "東京都", "03-1234-5678"]

id, name, tel = row.values_at(0, 1, 4)

id #=> "001"
name #=> "maimu"
tel #=> "03-1234-5678" 

パターンマッチで右代入風に値を取り出す

3つ目はパターンマッチの => を使って右代入風に値を取り出す方法です。

この記事では、変数代入の Tips としてパターンマッチを紹介しています。パターンマッチ自体は代入に限らず、条件分岐などでも使える汎用的な機能です。

docs.ruby-lang.org

Ruby/Rails Advent Calendar 2025の17日目の記事で sanfrecce_osaka もパターンマッチについて書かれています!

zenn.dev

そもそも、右代入とは?右代入風とは?という点についてまず触れたいと思います。

hash = { foo: 1, bar: 2, baz: 3 }

このようなハッシュがあって、一つ目の値を変数に入れたいとします。 普通に書くとこうなると思います。

foo = hash[:foo]

foo #=> 1 

これをパターンマッチの => を使って書き換えることができます。

hash = { foo: 1, bar: 2, baz: 3 }

hash => { foo: }

foo #=> 1

右代入のイメージが持てたでしょうか? 次に「右代入風」と書いていることについてです。上の例のように見た目は「右代入」のように使えるのですが、内部的にはパターンマッチの照合が行われています。そのため、マッチしない場合は例外が発生します。代入ではなくマッチングであるため、「右代入風」と表現しています。

hash = { foo: 1, bar: 2, baz: 3 }

hash => { piyo: }
#  {foo: 1, bar: 2, baz: 3}: key not found: :piyo (NoMatchingPatternKeyError)

パターンマッチではなく通常の代入を使うと例外ではなく、値が nil になります。

piyo = hash[:piyo]

piyo #=> nil

パターンマッチによる代入はハッシュのネストが深くなった時により便利さを実感できます。

user = {
  name: 'Alice',
  profile: {
    age: 10,
    address: 'Wonderland'
  }
}

パターンマッチを使わない場合はこうですね。

address = user[:profile][:address]

address #=> 'Wonderland'

パターンマッチを使う場合は以下のように書くことができます。

user => { name:, profile: { address: } }

address #=>  'Wonderland'

初めて見ると一瞬「なんだこれは・・・?」と思ってしまいそうですが、動きがわかると普段の実装にも活用できるシーンは色々ありそうです。 私はパターンマッチを使うことで期待している構造を前提にシンプルにコードを書ける点が気に入っています。

まとめ

私の好きな Ruby の変数代入 Tips として、partitionvalues_at、パターンマッチの => を使う方法について書いてみました。

何をしたいかやデータの構造に応じて使い分けて、これからもたくさん Ruby でプログラムを実装していきたいと思います!

【フロントエンド学習記録】マークダウンで任意の箇所に画像を挿入できるようにする

maimux2x.hatenablog.com

最後にフロントエンド学習の記事を書いてからだいぶ期間が空いてしまいました・・・。

今回は書きたいと思っていたマークダウンで任意の箇所に画像を挿入する実装を見ていきたいと思います。

バックエンドが Rails で、フロントエンドが Ember.js の組み合わせで開発していますが、フレームワーク固有な内容以外も含んでいるため、別のフレームワークで実装している場合でもある程度参考になる部分はあるのではないかと思います。

Ember.js 側の実装

maimux2x.hatenablog.com

こちらの記事で画像を複数枚登録できるようにした実装を修正していきます。 まず、現在は画像が固定箇所に挿入されているのでその部分のコードは削除します。

<!-- web/app/components/post.hbs -->
    </h2>
    <div class="card-text">{{this.renderMarkdown @post.body}}</div>
<!-- each ブロックの3行を削除する -->
    {{#each @post.image_urls as |url|}}
      <img src={{url}} alt="" class="img-fluid">
    {{/each}}
  </div>
</div>

続いて post-form.js に処理を追加していきます。

Ember.jsが提供する modifier を使って、ブログ記事を入力する textareaのDOM要素をコンポーネントインスタンスとして保持してアクセスできるようにしています。

// web/app/components/post-form.js

import Component from '@glimmer/component';
import { DirectUpload } from '@rails/activestorage/src/direct_upload';
import { action } from '@ember/object';
// ↓追加する
import { modifier } from 'ember-modifier';

export default class PostFormComponent extends Component {
// ↓3行を追加する
  setTextarea = modifier((textarea) => {
    this.textarea = textarea;
  });

// ...
}

modifier は DOM を操作したい場合に利用します。

guides.emberjs.com

DOM を操作する目的はマークダウン形式の画像リンクをテキストエリアの任意の箇所に挿入するために、現在のカーソル位置を取得→リンクを挿入→カーソル位置をリンクの最後に持ってくるなどの制御をするためです。 具体的な実装を見ていくと以下の手順に従って処理が行われています。

  1. カーソル位置の取得: selectionStartとselectionEndでテキストエリア内のカーソル位置を取得
  2. テキストの分割 : カーソル位置を基準に前後のテキストを分割
  3. 画像リンクの生成 : Active Storageのsigned_idを使用してMarkdown形式の画像リンク![filename](url)を作成
  4. テキストの結合と更新 : 分割したテキストの間に画像リンクを挿入してpostのbodyを更新
  5. UI更新 :
    • autosize.updateでテキストエリアのサイズを自動調整
    • カーソル位置を挿入したテキストの直後に設定
    • フォーカスを戻す
  6. 入力フィールドのクリア: ファイル選択をリセット
// web/app/components/post-form.js

export default class PostFormComponent extends Component {
  // ...
  uploadImage(e: Event) {
    
      upload.create((error, blob) => {
  // ...
        if (error) {
          console.error(error.message);
        } else if (blob) {
          // ↓1行削除
          this.args.post.images.push(blob.signed_id);
          // ↓追加するコード
          const textarea = this.textarea!;

          const startPos = textarea.selectionStart; // 手順1
          const endPos = textarea.selectionEnd;
          const before = textarea.value.substring(0, startPos); // 手順2
          const after = textarea.value.substring(endPos); 
          const text = `![${blob.filename}](${ENV.appURL}/rails/active_storage/blobs/redirect/${blob.signed_id}/${blob.filename})`; // 手順3

          this.args.post.body = before + text + after; // 手順4

          // 手順5
          runTask(this, () => {
            autosize.update(textarea);

            textarea.selectionStart = textarea.selectionEnd =
              startPos + text.length;
            textarea.focus();
          });
        }
      });
    }

    target.value = ''; // 手順6
 // ここまでが追加するコード
  }
}

続いて、web/app/components/post-form.js で DOM を操作するために追加した modifier を呼び出す修正をしていきます。

<!-- web/app/components/post-form.hbs -->
<!-- ... -->
<!-- 1行削除 -->
<Textarea @value={{@post.body}} id="body" class="form-control {{if @post.errors.body "is-invalid"}}" />
<!-- 1行追加 -->
<Textarea {{this.setTextarea}} @value={{@post.body}} id="body" class="form-control {{if @post.errors.body "is-invalid"}}" />

最後にマークダウンから画像タグ(<img>)を安全に表示できるように、HTMLのサニタイズ処理で画像タグを許可する修正をします。

// web/app/components/post.js

return htmlSafe(
  sanitizeHtml(marked.parse(body), {
    // img タグを許可する
    allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
  }),
);

続いて、Rails側の実装を見ていきます。

Rails 側の実装

フロントエンド側ではすでにマークダウンでブログの記事を書けるようになっていますが、記事のマークダウンを Rails 側でも扱えるようにする必要があります。そのために commonmarker というgem を使います。

github.com

データベースにブログ記事を保存する際に commonmarker で解析をして、画像情報の取得と保存をする処理を実装していきます。

  1. Markdownの解析: Commonmarker.parse(body)でpost本文をパースし、ASTを生成
  2. ノードの走査: walkメソッドで全ノードを巡回し、:linkと:imageタイプのノードを検出
  3. Active Storage URLの抽出: 正規表現で以下のパターンをマッチング:
    • /rails/active_storage/blobs/redirect/{signed_id}/
    • /rails/active_storage/blobs/proxy/{signed_id}/
    • 名前付きキャプチャ(?<signed_id>\S+)でsigned_idを抽出
  4. signed_idの収集: マッチしたsigned_idを配列に格納
  5. imagesカラムの更新: 収集したsigned_id配列をimagesカラムに保存
# app/models/post.rb
class Post < ApplicationRecord

# ...

  before_save :set images 

# ...

  private

  def set_images
    signed_ids = []
    # 手順1
    Commonmarker.parse(body).walk do |node|
      case node.type
      # 手順2
      when :image
        # 手順3
        if %r{/rails/active_storage/blobs/(?:redirect/|proxy/)?(?<signed_id>\S+)/} =~ node.url
          # 手順4
          signed_ids << signed_id
        end
      end
    end
   # 手順5
    self.images = signed_ids
  end
end

set_images メソッドは before_savePost が保存される直前にコールバックとして実行されるようにします。

また、今回はActiveStorage のダイレクトアップロードを使用しているため、クライアント側で画像を選択した時点で データベースに blob が作成されています。ブログ記事を保存前に画像を差し替えたりした場合、未使用の blob が残ってしまうため、それを削除するメソッドも用意します。

# app/models/post.rb

class Post < ApplicationRecord

# ...

  after_commit :purge_unattached_blobs

  private
  # ...
  def purge_unattached_blobs
    ActiveStorage::Blob.unattached.each(&:purge)
  end
end

purge_unattached_blobs メソッドは after_dommit でデータベースへの保存が確定した後に実行されるようにします。

まとめ

以上がマークダウンで任意の場所に画像リンクを挿入するための実装です。 DOM の操作や 画像リンクの組み立てなどとても勉強になる実装内容でした。

なるべく間が開かないように他の機能の実装についてもまとめていきたいと思います。 次は tag 検索について書きたいと思います!

関西Ruby会議08に参加した

6月28日(土)に京都で開催された関西Ruby会議08に参加してきました。

2年前の大阪Ruby会議03に参加して以来の関西圏でのカンファレンス参加でした。 開催後1週間が経っていますが、今振り返っても参加できてよかったな〜と温かい気持ちになる素敵な場でした!

発表の感想

「富岳」と研究者をRubyでつなぐ:シミュレーション管理ツールOACIS

スライドは公開されてなさそう? 研究機関の場で Ruby が活用されている事例でとても興味深い発表でした。

「1ヶ月でWebサービスを作る会」で出会った rails new、そして今に至る rails new

speakerdeck.com

個人開発でローンチするまでの流れを実体験をもとに丁寧に紹介されていて、自分もやるぞ!と背中を押してくれる発表でした。 サービスを個人で運用するにはサーバー代など費用が発生する訳で、その辺りについても工夫された点など言及されていて個人開発のリアルが詰まっていました。

ふだんのWEB技術スタックだけでアート作品を作ってみる

speakerdeck.com

個々のアナログ時計の集合がゆっくり時間をかけてデジタル時計としての表現に変わる様子がとても素敵な作品でした。 また、Vue.js 版、Rails版、Wasm版と技術構成を変えてトライされていた点について、全く分野は違うのですが私も自作ブログを技術構成を変えて複数作ったりしているため、同じものでも構成が違うと考える点が変わるんだよな〜とか聞きながら一人で共感していました。

発表後にソースコードを見たいです!とお声がけをしたところ、その日のうちに公開してくださったため速攻スターをつけました! 実装を参考にさせていただきつつ、自分もRailsでクリエイティブコーディングをやってみたくなっています。

分散オブジェクトで遊ぼう!〜dRubyで作るマルチプレイヤー迷路ゲーム〜

speakerdeck.com

dRuby は RubyKaigi で興味を持ちつつ自分自身ではまだ触れられていない技術だったため、実際に迷路ゲームを開発した体験談を聞くことができ、どういうことができて難しい部分は何だったのかを具体的に知ることができました。 ゲーム全体のアーキテクチャなども丁寧に解説されていて分かりやすかったです!

Rubyで世界を作ってみる話

こちらもスライドは公開されてなさそう? 予想ができない発表内容で聞いていてとても面白かったです。 また、スライドに散りばめられた松田さんのコードが切れ味が良くてかっこよかった。

Regional.rb and the Kyoto City

これは現地で聞くことができてとてもよかったと思えたセッションでした。

私自身、コミュニティをやっていて共感する部分がたくさんあったのと、ここでの話を聞いたことでしんめ.rbの取り組み方をちょっと見直してみようと迷っていたところから一歩踏み出す勇気をもらえました。

まとめ

今回、関西Ruby会議08に参加して一つ気がついたことがあって、私は「何を」作るかよりも「どう」作るかに今はすごく興味があるんだなということを自覚しました。 それもあって、自作ブログを技術構成を変えて作り続けたりできてるんだろうなと実感して一人納得していました。 ただ、発表を通して作りたいものを実現していく楽しさも思い出すことができ、「何をどう」作るかという部分を大切にしながら、今後も個人開発を楽しんでいきたいなと思いました。

運営・発表者の皆様、素敵なカンファレンスの場をありがとうございました! 来年の滋賀も参加するぞ〜!

しんめ.rb の開催について

2024年3月より開催を始めたしんめ.rb ですが、2025年7月以降は毎週開催をやめて『不定期開催』とすることにしました・・・!

7月は開催を行わず、8月以降は月2回程度を目安にこれまで通りオンラインで開催しようと考えています。

東京Ruby会議12や関西Ruby会議08に参加した際に地域rbを続けるコツは「無理しない」ことが挙げられていました。最近の私に関してはしんめでやりたいことは色々あるものの準備にそこまで時間をかけられなかったりで、自分の中に準備不足感が蓄積していたり、うまく進行できていないな〜という気持ちがあり、少し「無理」が生じ始めていました。

コミュニティを終わりにするかも考えたのですが、しんめを通じて関東圏以外の Rubyist たちと繋がることができ、カンファレンスなどのイベント参加がより一層楽しくなったこと、みんなでモブプロをしたり情報交換をする時間も大好きなことから、終わらせるのではなく開催頻度を減らして継続することにしました!

コミュニティを自分で主催していると失敗をしたり反省点は尽きず、課題もたまっていってしまったりします。 その一方で主催しているからこそ得られる良さもたくさんあります。

自分が忙しかったり、疲れていたりするとどうしても考えがネガティブ寄りに傾きがちなため、そいう時こそ無理をしないが大事だな〜1年間の開催を経て私自身も学ぶことができました。

8月以降は開催をする場合、曜日はこれまで通り月曜開催としますが、開始時間は20時からにしようと考えています(開始時間は21時からのままにするかもしれないため、次回開催日を決める際に告知します!)。

これからも RubyRails ともっと仲良くなりたい!みんなでワイワイ楽しく勉強する場としてしんめ.rb を育てていけたらと思います。

1ヶ月お休みをいただきますが、8月から改めてよろしくお願いします🌱

【フロントエンド学習記録】Ember.jsでブログ投稿画面のプレビューをできるようにする

前回はブログ記事をマークダウンで書けるようにする実装を見ていきました。

maimux2x.hatenablog.com

次は記事中の任意の場所に画像を埋め込めるようにする部分を見ていくと書いてたのですが、先にEmber.jsでブログ投稿画面のプレビューをできるようにする部分の実装を見ていきたいと思います。

画像のようにタブがあって、ブログ記事を書く部分とプレビューできる部分を作りました。

Rails のみでプレビューをできるようにする場合、Hotwire で TurboFrames や Stimulus を使って実装する形になると思うのですが、Ember.js ではすでにブログ記事を表示する役割を担っているコンポーネントがあるため、実はそれを再利用すればすぐに実装することができます。

そのため、やったこととしてはブログ投稿画面の hbs ファイルを Bootstrap のナビゲーションコンポーネントとタブJavaScriptプラグインを使ってタブを切り替えられるようにして、投稿用のタブに元々あったコードを移して、プレビュー用のタブでブログ記事を表示するための Post コンポーネントを呼び出す対応のみです!

getbootstrap.jp

<!-- web/app/components/post-form.hbs -->

<ul class="nav nav-tabs mb-3" id="postFormTab" role="tablist">
  <li class="nav-item" role="presentation">
    <button class="nav-link active" id="home" data-bs-toggle="tab" data-bs-target="#home-pane" type="button" role="tab" aria-controls="home-pane" aria-selected="true">Form</button>
  </li>
  <li class="nav-item" role="presentation">
    <button class="nav-link" id="preview" data-bs-toggle="tab" data-bs-target="#preview-pane" type="button" role="tab" aria-controls="preview-pane" aria-selected="false">Preview</button>
  </li>
</ul>
<div class="tab-content" id="postFormTabContent">
  <div class="tab-pane fade show active" id="home-pane" role="tabpanel" aria-labelledby="form-tab">
    <!-- 元々あった投稿用のフォームの実装 -->
  <div class="tab-pane fade" id="preview-pane" role="tabpanel" aria-labelledby="preview-tab">
    <Post @post={{@post}} /> <!-- Postコンポーネントを呼び出す -->
  </div>
</div>

まとめ

フロントエンド側のフレームワークコンポーネント化されているパーツの再利用が便利だな〜と感じた瞬間でした。

また、Bootstrap もWeb サービスで頻繁に利用される UI を実現するためのコンポーネントなどがたくさんあって、同様に便利だなと思ったのともっと使いこなせるようになりたいと思いました。

順番を入れ替えたため、次回に記事中の任意の場所に画像を埋め込めるようにする実装を見ていきます!

【フロントエンド学習記録】マークダウン形式でブログを投稿できるようにする

前回はブログに複数枚の画像を添付する実装を見ていきました。

今回は画像をブログ本文の任意の場所に挿入できるようにする前準備としてマークダウンで記事を書けるようにしていく実装を見ていきます。

マークダウン形式で記事を書けるようにするには、現段階では投稿フォーム側で特に実装を変更する箇所はありません。対応が必要なのはブログ本文を描画する側の実装です。

maimux2x.hatenablog.com

実装にあたり Marked というライブラリを使いました。

github.com

Marked を利用することでマークダウン形式のテキストを HTML に変換することができます。 利用する上では README にも記載がある通り、Marked のみでは HTML をサニタイズすることができないため、別のライブラリと併用する必要があります。

github.com

今回はMarked の README に記載されていた sanitize-html を使ってみました。

これらを利用して本文を引数に取り HTML への変換とサニタイズ、HTML としてレンダリングするためのアクションを用意します。

// web/app/components/post.js

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { marked } from 'marked';
import { htmlSafe } from '@ember/template';
import sanitizeHtml from 'sanitize-html';

export default class PostComponent extends Component {
  @action
  renderMarkdown(body) {
    return htmlSafe(sanitizeHtml(marked.parse(body)));
  }
}

Marked と sanitize-html を使ってブログ本文をマークダウンから HTML へ変換し、サニタイズしている部分が sanitizeHtml(marked.parse(body) です。そして、変換された HTML を Ember.js へ HTML としてレンダリングすることを伝えるための関数が htmlSafe() の部分です。

このアクションを以下のように使用することで、マークダウン形式で保存されているブログ本文が適切に変換された結果として描画することができます。

<!-- web/app/components/post.hbs -->

<div class="card mb-3">
  <div class="card-body">
    <h2 class="card-title">
      {{#if @link}}
        <LinkTo @route="posts.show" @model={{@post.id}}>{{@post.title}}</LinkTo>
      {{else}}
        {{@post.title}}
      {{/if}}
    </h2>
    <div class="card-text">{{this.renderMarkdown @post.body}}</div> <!-- @post.body をアクションに渡す -->
    {{#each @post.image_urls as |url|}}
      <img src={{url}} alt="" class="img-fluid">
    {{/each}}
  </div>
</div>

これで実装完了です!

まとめ

最低限のマークダウンでブログ本文を書くようなユースケースであれば、ライブラリのおかげでスムーズに対応できることが分かりました。

投稿フォーム側でできることを増やしていこうとすると、実装上色々考えないといけない部分が出てきそうです。次はその事例の一つとして画像をマークダウン本文内の任意の位置に挿入できるようにする部分の実装を見ていきたいと思います!