Torihaji's Growth Diary

Little by little, no hurry.

prismaの定義をキャメルケースにしたいと思ったら

はじめに

みなさん、こんにちは torihaziです

最近、frontendにおいてdataの型管理効率化のためだけに prismaを入れて開発を進めてます

最初はコマンド1つでDBからスキーマ取得して云々作成してくれることに

めっちゃ感動していたのですが使っていくうちに、「およよ?」ということが増えてきました

今回のタイトルもその「およよ?」のうちの1つです

経緯

backendはRails(スネークケース)、frontendはNext.js(キャメルケース)で開発をしています。

最近、frontendからbackendにpostするときにaxiosのinterceptor使って

スネークケースにしたり、取得するときにキャメルケースにしたりすることを知りました。

qiita.com

知ったのはいいのですが、今自分がprisma使ってやっているものだとデフォルトのprisma

スネークケースでpullしてきてしまうので「どうしたものか」と思って調べたのが今回記事作成に至った経緯です。

結論

あらかじめprisma入っている前提で話を進めます。

入ってない方はこれみて雰囲気でも掴んでください笑

Nextjs x Rails のXクローンにprismaを入れて楽(?)をする。 - Torihaji's Growth Diary

話は戻って、、

Prismaのschema定義で、DBのカラム名はsnake_caseだけど実際に使う時はcamelCaseにしたい人へのライフハック #TypeScript - Qiita

こちらの方を参考にしました。というかまんまですね。

prisma-case-format - npm

これ入れて、

package.json

    "pull": "prisma db pull",
    "generate": "prisma-case-format --file prisma/schema.prisma && prisma format && prisma generate"

として

npm run pull
npm run generate

とすればずばーんとできます

これを途中で導入した場合、色々旧prismaの定義で作ったものを全て置換する必要がありますが

それは Linuxとか Vscodeの置換機能使ってガンバッテクダサイ。

自分もそうしたので笑

終わりに

自分が気になることは大抵他の誰かが手をつけているということが改めて分かりました

世界って広いなと笑

頑張りましょう。

alba用の"rails g alba hoge"みたいなジェネレータを作った

はじめに

みなさん、こんにちは torihaziです

今日は昨日初めて知ったRailsの自作ジェネレータを実際に使ってみたということで練習がてら作ってみました

昨日の時点で便利では?と思ったので今後も使っていきたいと思います

ちなみにalbaはこれです。Railsのシリアライザ?です。

https://github.com/okuramasafumi/alba

経緯

今作っているXクローン用に昨日ずっとRailsのSerializerを探していて、jsonapi-serializerとか色々触ってみたのですがどれも「うーん」ということで辿り着いたのが albaでした。

その時の体験記がこれです。

https://torihazi.hateblo.jp/entry/2024/10/25/220337

このついでに毎回ファイル手動で作るのだるいとなってrails g serializerがあることを知り、さらにこの"ジェネレータ"自体が自分でも作れるのを知りました。

で、albaが見てみた感じなさそうだったのでその時得た知識をもとに作ってみよう!となって今に至ります。

その前に

導入方法とかはこの方のものを参考にしてください

https://zenn.dev/hujuu/articles/implement-alba-in-rails

どういうものか

結論言うと、

rails g alba tweetWithImages --model tweet

とするとapp/resources配下にファイルが出来ます。 なければ作成されます

# frozen_string_literal: true

class TweetsWithImagesResource < BaseResouce
  root_key :tweet

  attributes :id
end
class BaseResource
  include Alba::Resource
end

ちなみにnamespace作って云々したければ

rails g alba hoge/hoge --model hoge

とすればapp/resources/hoge配下にファイルが出来ます。 なければ作成されます。

# frozen_string_literal: true

module Hoge
  class HogeResource < BaseResouce
    root_key :hoge

    attributes :id
  end
end

こっちもbase_resource.rbがなければできます。

とこんな感じ。

いざ

ジェネレータ作ります。

rails g generator alba

結果、lib/generators/alba配下に色々ファイルやらディレクトリができます。

まず触るのはalba_generator.rb

class AlbaGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  class_option :model, type: :string, default: 'sample'

  def create_alba_base_file
    base_resource_path = File.join('app/resources', 'base_resource.rb')
    
    unless File.exist?(base_resource_path)
      create_file base_resource_path, <<~RUBY
        class BaseResource
          include Alba::Resource
        end
      RUBY
    end
  end

  def create_alba_file
    template 'alba_template.erb', File.join('app/resources', class_path, "#{file_name}_resource.rb")
  end

  def run_rubocop
    generated_file_path = File.join('app/resources', class_path, "#{file_name}_resource.rb")
    system("bundle exec rubocop -a #{generated_file_path}", out: File::NULL, err: File::NULL)
  end

  private
  def model
    options['model']
  end
end

下記を見れば大体書いているので参考にしてください。なければAiに投げてください

https://railsguides.jp/generators.html

ポイントをかいつまんで説明すると

  • 上から下へ順に def で定義したパブリックなメソッドが実行されます
  • class_optionはコマンドの引数を追加したい場合設定します
  • class_optionで定義した引数はoptions['引数名']で取得します
  • templateメソッドは テンプレートファイルを第1引数に、それを元に作るファイルのパスを第2引数にとります
  • コード整形用にrubocop使ってます

くらいです。頭のいいみなさんなら秒なので多分平気です。

次にtemplates/alba_template.erbを作成し、記述します。

# frozen_string_literal: true
<% module_namespaces = class_path.map(&:camelize)%>
<% if module_namespaces.any? %>
<%= module_namespaces.map{|namespace| "module #{namespace}"}.join("\n") %>
  class <%= file_name.camelize %>Resource < BaseResouce
    root_key :<%= model %>

    attributes :id
  end
<%= "end\n"*module_namespaces.size%>
<% else %>
class <%= file_name.camelize%>Resource < BaseResouce
  root_key :<%= model %>

  attributes :id
end
<% end %>

てすれば、もうあとは rails g alba ファイル名 --model 対象のmodelを実行すればいけます。

終わりに

rubyの記述あれな場合は、上手い具合に改造しちゃってください

ということで。

色々使ってみてわかったイチオシのRails Serializer

はじめに

みなさん、こんにちは torihaziです

今日はタイトル通り 数あるRailsのSerializerのgemを色々使ってみて

どれが自分にとって使いやすいかを検証していこうと思います。

メンテがされてるされてない、重い重くないとかは気力があったら書いていきます。

とにかく使いやすいか否か。

ここでいう使いやすいか否かの判断基準は 定義とかfrontendでの取り出し方とかです

前提条件

筆者はfrontendから useSWR x axiosを使ってデータを取得しようとしています。

そのためレスポンスの形は

interface AxiosResponse<T = any> {
  data: T;          // 実際のレスポンスデータ
  status: number;
  statusText: string;
  headers: any;
  config: any;
}

こんな感じで帰ってきます。これを前提としてbackendでSerializer使ってきます

エントリーNo.1 jsonapi-serializer(fast_jsonapi)

GitHub - jsonapi-serializer/jsonapi-serializer: A fast JSON:API serializer for Ruby (fork of Netflix/fast_jsonapi)

gemfileに

gem 'jsonapi-serializer'

してbundle installした後にapp/serializers/ファイル名_serializer.rb作って

# frozen_string_literal: true

class TweetIndexWithImagesSerializer
  include JSONAPI::Serializer

  set_type :tweet

  attribute :id, :content, :image_urls
end

とする。

でcontrollerで

      def index
        tweets = Tweet.all
        json_string = TweetIndexWithImagesSerializer.new(tweets).serializable_hash
        render json: { message: '成功', data: json_string }, status: :ok
      end

みたいにして呼ぶとレスポンスは

data: { 
    data: {
        data: {
            0: {
                id: 数字
                type: tweet
                attributes: {
                    content: "hoge"
                    image_urls: [~~~~,~~~・・・]
                }
           }
           ,・・・・
         }
     }
}

何個data のkeyがあるんじゃい。

1個目はaxiosのdata、2個目は rails のcontrollerで定義したやつ、3個目はjsonapi-serializerのやつ。

あとserializerでactivestorageのurl返す時詰まったら下記を参照

Serializerで画像のurlをjsonで返す際に発生したエラー #Ruby - Qiita

ok。次。

エントリーNo2 activemodel-serializer

色々触ろうと思いましたが、GithubのReadmeに「いったん開発やめるわ」みたいなこと書いてあったので却下。

ただ代替案をご丁寧に書いてくれていたのでそれをみてみることに。

エントリーNo3 jsonapi-rails

代替案1つ目らしいっす。

Start by adding the following to your Gemfile:

gem 'jsonapi-rails'
Then, once your serializable resources are defined, building a JSON API document from your business objects is straightforward: simply pass them to the render method.

gem入れて

render jsonapi: Tweet.all てすればいけるらしいけど。

ドキュメント見てわかったけど、ドキュメントがしょぼい気がする。

不親切といった感じ。自分の読解力がないだけかもしれない。

とりあえず使ってみて、使い方は2つ?あるっぽい。使い方が悪いのかもしれないけど

app/serializers/serializable_tweet.rb作って

# app/serializable/serializable_tweet.rb
class SerializableTweet < JSONAPI::Serializable::Resource
  type 'tweets'

  attributes :content, :image_urls  # 必要な属性を指定
  

  # 関連付けがある場合
  # has_one :user
  # has_many :likes
end

てした後に コントローラで

render jsonapi: Tweet.all

て感じで呼び出すと

data: { 
    data: {
        0: {
            id: 数字
            type: tweets (<= typeで設定した時の文言、してなければunknown)
            attributes: {
                content: "hoge"
                image_urls: [~~~~,~~~・・・]
            }
       }
       ,・・・・
     },
    jsonapi: ・・・
}

attributesていうのは出るっぽい。ちなみに前みたいに

render json: {jsonapi: Tweet.all, message: "成功"}

てやったとしてもなんかいけた。

そうしたら

data: { 
    jsonapi: {
        0: {
            id: 数字
            content: "hoge"
            created_at: 
            updated_at: 
       }
       ,・・・・
     },
}

モデルTweetが持つ全カラムが出力されたが、image_urlsが消えた。あとtypeの記載も。

attributesも消えたけどなんかバグっぽい。

これもないかな。

エントリーNo4 jsonapi_resources

jsonapi-resources.com

んーよくわからん。

次。

エントリーNo 5 alba

名前かっこいい。笑

それに最高。

求めてた形で帰ってきた。

設定方法はこれを参照

【Rails】AlbaでAPI実装

config/initializer/alba.rbに

# alba.rb
Alba.backend = :active_support
Alba.inflector = :active_support

てして

app/resources/base_resource.rbに

class BaseResource
  include Alba::Resource
end

app/resources/tweets_with_images_resource.rbに

class TweetsWithImagesResource < BaseResource
  root_key :tweet <= 対象のモデル

  attributes :id, :content, :image_urls
end

ちなみにimage_urlsはtweet.rbにおいてこう定義してる。

# frozen_string_literal: true

class Tweet < ApplicationRecord
  include Rails.application.routes.url_helpers

  belongs_to :user
  has_many_attached :images

  def image_urls
    images.map { |image| url_for(image) } if images.attached?
  end
end

コントローラは

tweets = Tweet.all
render json: {message: '成功', data: TweetsWithImagesResource.new(tweets)}

としたらレスポンスは

data: { 
    data: {
        0: {
            id: 数字
            content: "hoge"
            image_urls: [~~~~,~~~・・・]
        }
      ,・・・・
     }
}
config: ・・・

みたいな感じで理想のができた

attributesとかも出てないし、typeも出てないし。

君に決めた!

終わりに

長きにわたる最適なRails Serializer探しの旅終了。

選ばれたのは albaでした。

github.com

異論認めますが、いったんこの子を採用して個人開発進めます。

ということで。

rails g serializer の再発明。(jsonapi_serializerでも使えることを知らなかった)

はじめに

みなさん、こんにちは torihaziです

今日はもしかしたら車輪の再発明みたいなことをしてしまったかもしれません。

あるの知りませんでした。

jsonapi_serializerを使っていて

rails g serializer userみたいなことをするときに

できないんじゃね?と思ってしまってあーだこーだしながら作ってみました。

今調べたらあったみたいです。

経緯

rails g serializer hogeみたいにしたらserializersディレクトリ配下に

hoge_serializer.rbみたいなファイルができて欲しかったので

それをコマンドラインからできる術を調べていました。

なにしろめんどくさかったので。

とりあえずrails generate generator hogeを使うと

lib/generaters/hogeディレクトリとか色々作られることがわかりました。

んで、active_model_serializersだとそれがあるけど

jsonapi_serializerだとそれがないから作るしかない、みたいな結論に

その時はなったので今回作りました。

ゴール

今回はrails g serializer hoge/hoge/hogeとしたら

app/serializers/hoge/hogeディレクトリ配下にhoge_serializer.rbができることまでをゴールにします。

手順

rails generate generator serializer

=> lib配下に色々ファイルができる。

lib/generaters/serializerのserializer_generator.rbに下記を追記

class SerializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path("templates", __dir__)

  def create_serializer_file
    template 'serializer.erb', File.join('app/serializers', class_path, "#{file_name}_serializer.rb")
  end

  def run_rubocop
    # ジェネレートしたファイルのパス
    generated_file = File.join('app/serializers', class_path, "#{file_name}_serializer.rb")
    
    # コード整形のためにRubocopを実行
    system("bundle exec rubocop -a #{generated_file} > /dev/null 2>&1")
  end
end

そしてlib/generaters/serializer/templates配下のserializer.erbを作って

# frozen_string_literal: true
<% module_namespaces = class_path.map(&:camelize) %>
<% if module_namespaces.any? %>
<%= module_namespaces.map{|namespace| "module #{namespace}"}.join("\n")%>
  class <%= file_name.camelize %>Serializer
    include JSONAPI::Serializer

    attributes :id
  end
<%= "end\n"*module_namespaces.size %> 
<% else %>
class <%= file_name.camelize %>Serializer
  include JSONAPI::Serializer

  attributes :id
end
<% end %>

と記載

使うときは rails g serializer hoge/hoge/hgoeとかrails g serializer hogeです。

namespaceつけたいなら前者だし、そうじゃないなら後者みたいな。

そうすれば前者だとこんなのできる

# frozen_string_literal: true

module Hoge
  module Hoge
    class HogeSerializer
      include JSONAPI::Serializer

      attributes :id
    end
  end
end

説明

説明とまではいかないかもしれません。

コード整形はrubocop使ってます。

で標準出力でるとなんかダサいと思ったので全部/dev/nullに流してます。

あとは自信が詰まった箇所を箇条書きで。

  • メソッドは上から順に実行される lib/generaters/serializerのserializer_generator.rbに defで色々メソッド書きましたが 上から実行されます。
  • __dir__ その実行ファイルがあるディレクトリの絶対パス
  • templateメソッド template(templateファイルがあるパス, 出力先のパス)

詳しいこと書いてます https://railsguides.jp/generators.html

終わりに

まぁ新しく知識増えたのでよしとしましょう。

rails g generatorなんて知りませんでした。

axiosのinterceptorを使って、submitするdataを全てスネークケースにする方法

はじめに

みなさん、こんにちは torihaziです

今日は実務でタイトルのことをやる機会があって、

色々と調べたのでそれを書き残そうかと思って今書いてます

経緯

frontendとbackendでキャメルケースかスネークケースかで違うと思います。

frontはuserDataみたいなキャメルで、backはrailsだとuser_dataみたいな

スネークケースですよね。

今までもスネークケースにする処理はfrontでやっていたり、

それか

user_data: {
  name: data.hoge
  age: data.age
  ・・・
}

みたいにしてわざわざベタ書きしている箇所もあり統一性がありませんでした。

後何よりもし保守性がよろしくない。

ということでこれを機に手をつけたというわけです。

どうやるか

import { snakeCase } from "change-case";

const METHOD_WITH_PAYLOAD = ["post", "put"];
type inputType = object | object[]

const convertToSnakeCase = (input: inputType): any => {
  if (Array.isArray(obj)) {
    return obj.map(convertToSnakeCase);
  } else if (obj !== null && typeof obj === "object") {
    return Object.entries(obj).reduce(
      (result, [key, value]) => ({
        ...result,
        [snakeCase(key)]: convertToSnakeCase(value)
      }),
      {}
    );
  }

// api: axiosのinstance
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {

  if (
    config.method &&
    METHOD_WITH_PAYLOAD.includes(config.method.toLowerCase())
  ) {
    config.data = convertToSnakeCase(config.data);
  }

  return config;
}

  

これをすれば、postかputの時だけ全部勝手にsnakecaseに変換してbackendに飛ばしてくれます

終わりに

以上!

実務でやって初めてこういう変換処理を加えるべきなんだと思いました

まだまだ発展途上です

参考

https://axios-http.com/ja/docs/interceptors

TypeScriptでオブジェクトの型からキーのみを取り出してユニオン型を作るときの落とし穴

はじめに

最近知りました。

結論

オブジェクト型A、オブジェクト型Bのユニオン型からなる型Cに対してkeyofを設定すると

AとB共通でもつkeyのみのユニオン型が返される。

やりたいなら type C = (keyof A) | (keyof B)

経緯

TypeScriptにおいてオブジェクトの型からキーのみの型を取り出したいとする。

https://typescriptbook.jp/tips/generates-type-from-object-key

例えばこういう型があったとする。

export type UserBase = {
  id: number;
  name: string;
  role: "student" | "mentor";
  email: string;
  age: number;
  postCode: string;
  phone: string;
  hobbies: string[];
  url: string;
};

export type Student = UserBase & {
  studyMinutes: number;
  taskCode: number;
  studyLangs: string[];
  score: number;
  availableMentors?: string[];
};

export type Mentor = UserBase & {
  experienceDays: number;
  useLangs: string[];
  availableStartCode: number;
  availableEndCode: number;
  availableStudents?: string[];
};

export type User = Student | Mentor;

このような型の時にUserというオブジェクト型のキー名全部を取り出したいとき。

欲しいのは

type HOGE = "id" | "name" | ・・・

みたいな。

これを取得するために

export type UserKey = keyof User

としたらそれはアウトです。

結論で言ったことが起こります。

この場合ならUserKeyに入るのは BaseUserの型が持っているkey。

だからもしやりたいなら

type hoge = keyof Student | keyof Mentor;

終わりに

何度も言います。

初めて知りました。

BlockNote を使って遊んでみる.

はじめに

BlockNoteはWYSWIGなEditor。

What you see is what you get 。

みたまんまのものをgetできるよ。というエディタ。

MarkdownとかHTMLとかも行けるみたいで、 Editorjsと違ってなんか強そうということで触ってみる。

深掘りはやる気次第。

敵情視察みたいな感じ。

Next.jsのinstall

npx create-next-app@latest
Need to install the following packages:
create-next-app@14.2.15
Ok to proceed? (y) y

✔ What is your project named? … block_note_sample
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like to use `src/` directory? … No
✔ Would you like to use App Router? (recommended) … No
✔ Would you like to customize the default import alias (@/*)? … No

少ししたら作れる。

cd block_note_sample

試しにローカルサーバを立ち上げる

npm run dev

http://localhost:3000 開いて下記の画面が開いたらOK

githubのセットアップ

github開いて、リモートリポジトリ作って、リポジトリの設定

git remote add origin https://github.com/(ユーザ名)/block_note_sample.git
git branch -M main
git push -u origin main

BlockNoteを入れる

www.blocknotejs.org

基本的にこの流れに沿っていく。

ものを入れる

npm install @blocknote/core @blocknote/react @blocknote/mantine

入ったので試しに 公式のまんま使ってみる。

pagesディレクトリ配下に note.tsxを作成してその中に、

import { BlockNoteView } from "@blocknote/mantine";
import { useCreateBlockNote } from "@blocknote/react";

const NoteIndex = () => {
  // Creates a new editor instance.
  const editor = useCreateBlockNote();

  // Renders the editor instance using a React component.
  return <BlockNoteView editor={editor} />;
};

export default NoteIndex;

ローカルサーバ立ち上げて、localhost:3000/note にアクセス

よくあるやつ。

SSRだから、サーバの方でブラウザ用のグローバル変数(window)を使おうとして怒られてる。

Next.jsで「document is not defined」と怒られたときの対処法 #React - Qiita

これはEditorjsの時でもあった。

このようなエディタのライブラリは クライアントサイドでのみ動くもの。

事実、公式のドキュメントにもそう書いてある。

Next.js usage (or other server-side React frameworks) Are you using Next.js (create-next-app)? Because BlockNote is a client-only component, make sure to disable server-side rendering of BlockNote. Read our guide on setting up Next.js + BlockNote

ということで公式の言う通りの対処を実施する。

今回は dynamicインポートを使う。

Next.js and BlockNote - BlockNote

ということで新しく componentsディレクトリを作成し、その下にeditor.tsxを作成して下記を記述

import { BlockNoteView } from "@blocknote/mantine";
import { useCreateBlockNote } from "@blocknote/react";

const Editor = () => {
  // Creates a new editor instance.
  const editor = useCreateBlockNote();

  // Renders the editor instance using a React component.
  return <BlockNoteView editor={editor} />;
};

export default Editor;

次に元々書いていたnote.tsxを以下のように修正

import dynamic from "next/dynamic";

const Editor = dynamic(() => import("../components/editor"), { ssr: false });

const Note = () => {
  return (
    <div>
      <Editor />
    </div>
  );
};

export default Note;

localhost:3000/note を見てみると

あら。なんかおかしい。

あれか、cssつけてないからか。

ということで公式のもの入れ忘れたので入れる

import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css";

うん、なった。これで無料か。すごい。

Editorjs入れる時あれだけ苦労したのに。

操作感はめちゃめちゃいい。

Notionみたい

この2、3行書いただけで分かったこと。

良いとこ

  • Header のデザインが綺麗
  • コマンドショートカットがある。
  • 番号付きリスト、箇条書きのリストにおいて綺麗
  • リストにおいてTab 、シフト + Tabで階層移動ができる。
  • 階層移動の際の動きがスムーズ。
  • チェックボックスがあり、階層構造がある。
  • インラインツールバーが充実してる。(文字太くしたり文字色変えたり色々できる)
  • 表も記述可能。

悪いとこ

  • 画像や動画、ファイルの添付ができない?
  • Youtubeの共有URL貼り付けても文字列として認識される?

=> 多分画像とかは追加で設定する必要があるのか、

ただNo Configでここまで行けるのはすごいと思う。

もう少し続けてみる。

とにかく今やったことはこの2つだけ。

  • pages/note.tsxに公式のものをコピーしただけ。
  • components/editor.tsxに公式のものをコピーしただけ。

これだけでここまで行けるのは。何度も言うけどすごいと思う。

もう少し深掘り

  const editor = useCreateBlockNote();

こいつに引数は設定可能らしい。

function useCreateBlockNote(
  options?: BlockNoteEditorOptions,
  deps?: React.DependencyList = [],
): BlockNoteEditor;

とりあえずoptionsという方に色々設定可能みたい。

Editorjsのtoolに相当するものらしい。

type BlockNoteEditorOptions = {
  initialContent?: PartialBlock[];
  domAttributes?: Record<string, string>;
  defaultStyles?: boolean;
  uploadFile?: (file: File) => Promise<string>;
  collaboration?: CollaborationOptions;
  dictionary?: Dictionary;
  schema?: BlockNoteSchema;
  trailingBlock?: boolean;
  animations?: boolean;
};

Editorjsのdataに相当するのはこのinitialContentらしい。

あと関係ありそうなのは uploadFileとかいう関数

次はこれ。

<BlockNoteView editor={editor} />

実際のview。

export type BlockNoteViewProps = {
  editor: BlockNoteEditor;
  editable?: boolean;
  onSelectionChange?: () => void;
  onChange?: () => void;
  theme?:
    | "light"
    | "dark"
    | Theme
    | {
        light: Theme;
        dark: Theme;
      };
  formattingToolbar?: boolean;
  linkToolbar?: boolean;
  sideMenu?: boolean;
  slashMenu?: boolean;
  emojiPicker?: boolean;
  filePanel?: boolean;
  tableHandles?: boolean;
  children?:
} & HTMLAttributes<HTMLDivElement>;

渡せるのはこいつららしい。

ぱっと見使えそうというか、関係ありそうなのは onChangeとか。

エディタの内容が変わるのを検知して発火するみたい。

実際にbackendとかと連携して使っていくとなるとこの子を設定する必要があると思う。

出力形式は JSONみたい。特定の型に沿ったJSONを吐き出すみたい。

これもEditorjsと同じみたい。

JSON.stringfyして云々するやつ。

なんかここまで使ってみたけどなんとなくEditorjsと同じ感じで使っていけそうということがわかった。

さらなる深掘りはまた今度にしよう

飽きた。

終わりに

ほー。

強そう。何より"おしゃれ"。

ただtailwindcss使ってないので入れたらどんな感じで競合するかはわからない。

またBlocknoteのcssと競合しないかが心配。

Editorjsの時は Headerがただの文字列として表示されて、原因がtailwindだったということがあったから

まぁ一旦終了。