カテゴリー: HP制作

  • あなたのブログのURLは 日本語のまま

    あなたのブログのURLは 日本語のまま

    そもそも、こんな素朴な疑問を持たれるのではないでしょうか。
    インターネット上では様々な意見が飛び交っているというのが私の印象ですが、結局突き詰めるとSEOとしてはどうなのかということが焦点になると思います。
    では、その点についてはどうなんでしょうか?
    中にはSEOの観点から問題があるということを書いてあるものもあります。
    しかし、どうやらそれは違うように思われます。

    最近はやりのChatGPTで、こんな質問をしてみました。
    「日本語のURLはSEOにとって良くないというのは本当ですか?」
    結論としては次のとおりです。
    「一般的に、日本語のURLがSEOにとって良くないとは言えません。実際に、日本語のURLがSEOに有利に働く場合もあります。」
    たしかに、日本人にとっては英訳されたURLより日本語のままの方が分かりやすいですよね。
    だったらなぜ手間をかけて英訳してURLを英語にしなくてはならないのでしょうか?

    このように表示されたURLなら問題はありません。

    しかし、皆さんはこんな表示を見たことがありませんか?
    これは日本語エンコードという処理を通すと表示される現象です。実はこれでも、もとの日本語がUTF-8という文字コードでできていればSEO上の問題はないようです。
    これもChatGPTで尋ねてみました。
    「エンコードされた日本語のURLにSEO上の問題はありませんか?」
    回答としては
    「エンコードされた日本語のURL自体には、直接的なSEO上の問題はありません。つまり、エンコードされた日本語のURLを使用しているからといって、検索エンジンでの順位が下がるわけではありません。

    ただし、エンコードされた日本語のURLは、人間が読みにくく、理解しにくい場合があります。このため、ユーザーがクリックする可能性が低くなることがあり、それによって間接的にSEOに影響を与える可能性があります。また、エンコードされたURLを検索エンジンが正しく解釈できない場合もあるため、クローリングやインデックスに問題が生じる可能性もあります。」

    一見、問題ないという回答のように聞こえますが、「ユーザーがクリックする可能性が低くなる」のは良くないですよね。「人間が読みにくく」なんて言っていますけど、可読性は0です。それで本当にSEO上も問題がないと言えるのか甚だ疑問です。
    さらに個人的な感覚ですが、URLに日本語が入っているのは、全体のバランスが悪いと思います。全部日本語に統一できるのならいいですが、ドメイン部分は英語にならざるを得ず、いわゆるパーマリンクと言われる部分だけが日本語なのです。
    ということで私の結論は「やっぱりURL英語に統一しておく方がよい」ということです。

    しかし、仮に英語が堪能な方であったとしても、日本語でついていタイトルを英訳してURLに置き換えるのは相当面倒です。
    翻訳ツールを使うのは当たり前だとしても、タイトルをコピーして翻訳結果を出してそれをコピーしてURL欄に貼り付けるわけです。
    たまにしかブログを書かない方ならともかく、毎日日課として書いていらっしゃる方や、スマホで処理していらっしゃる方はなかなかできないんじゃないでしょうか?
    しかも、それほどSEO上の問題もないと思えば、ちょっとした一手間とはいえ、つい省略してしますでしょう。
    でも、プラグイン一つで勝手に変換してくれるんならどうでしょう?
    それなら、エンコードによる文字化けはなく、全体としてバランスが取れたURLになるということの方に魅力を感じるのではないでしょうか。

  • セットしたはずのグラデーションが表示されない?!

    セットしたはずのグラデーションが表示されない?!

    まずはPanelColorGradientSettingsを使ってみよう

    ブロック開発でカスタムブロックに色を設定するのは基本中の基本ですよね。
    まず、最初のカスタマイズは色の設定でしょう。
    WordPressはそのための入力用コントロールとしてPanelColorGradientSettingsというコントロールを用意してくれています。
    これが優れもので、中間色はもちろんのこと、グラデーションまでセットできます。
    これを使うようになってグラデーションを採用することが増えました。

    このコントロールを使って、単色でもグラデーションでも選択できるようにするには、次のような手順を踏みます。

    1. 色情報を格納するためにblock.json内にattributesを設定する。
    2. サイドバーにPanelColorGradientSettingsを設置する。
    3. ユーザーが選択した色をbackGroundスタイルにセットする。

    具体的なコードで見てみましょう。
    まず、block.jsonにこんなふうに入れます

    "attributes": {
        "bg_Color": {
            "type": "string",
            "default": "#504237"
        },
        "bg_Gradient": {
            "type": "string",
        }
    }
    

    attributesには初期値も設定できるんです。ですから、このように設定すると、最初にブロックがマウントされてattributesが呼び出されるとbg_Colorには#504237がセットされた状態になるのです。便利なので重宝します。

    つぎにPanelColorGradientSettingsの設置です。
    edit.jsのreturn文にInspectorControlsを用意してその中に次のコードを入力してやります。

    <PanelColorGradientSettings
        title={__("Background Color Setting","text-domain")}
        settings={[
            {
                colorValue: bg_Color,
                gradientValue: bg_Gradient,
    
                label: __("Choice color or gradient","text-domain"),
                onColorChange: (newValue) => {
                    setAttributes({ bg_Color: newValue });
                },
                onGradientChange: (newValue) => {
                    setAttributes({ bg_Gradient: newValue });
                },
            }
        ]}
    />
    

    これで取得したプロパティ値をDOM要素のstyle属性としてセットすればあっという間にインラインスタイルが出来上がります。
    次のような感じです。

    const bgColor = bg_Color || bg_Gradient;
    return(
        <div style={{ backGround: bgColor }}></div>
    )
    

    なんとも小気味よくセットできるのです。

    グラデーションが表示されない

    しかし、ふと見るとこんな現象が起こっています。

    お判りでしょうか。サイドバーには確かに選択したグラデーションが表示されているのに、レンダリングされたブロックは単色です。
    この色はデフォルトで設定した色です。
    なぜ、こんなことが起こるのか全く分かりません。

    そしてさらにsave.jsに

    <div style={{ background: bgColor }}></div>
    

    などとするとBlock validation failedというエラーを起こします。これはエディタでブロックが保存された状態と、それをレンダリングしようとする状態とで一致しない場合に発生します。リカバリを試みることはできますが、毎回そんなことをしているわけにはいかないし、実際リカバリもできもせん。
    途方に暮れてしまいました。

    原因の解明

    何とか原因が究明できましたので、ここでそれを解説します。

    PanelColorGradientSettingsは単色を選択するとその色のコードを、グラデーションを選択するとlinear-gradientまたはradial-gradientを返します。そして選択しなかった方はundefinedを返すのです。
    つまり、単色が選択されると、bg_Colorには色コードがセットされますが、グラデーションが選択されるとundefinedがセットされます。
    この仕組みが元凶でした。
    もう少し具体的に説明します。
    グラデーションを選択したとします。
    セットしたときはbg_Colorにundefinedがセットされ、bg_Gradientには、linear-gradientまたはradial-gradientがセットされます。
    そして、

    const bgColor = bg_Color || bg_Gradient;
    

    が実行されれば、無事にbgColorにはグラデーションのスタイルがセットされ、ブロックにはグラデーションがレンダリングされます。
    しかし、その状態でブロックが保存され、次にそのブロックがマウントされるとどうなるでしょうか。
    ここで、あの重宝していた初期値の設定が仇になります。
    bg_Colorはundefinedのままでいてくれません。attributesで設定した初期値である#504237がセットされるのです。そしてconst bgColor = bg_Color || bg_Gradient;が実行されるとbgColorには bg_Colorがセットされてしまって bg_Gradientは無視されてしまいます。
    これが、奇妙な現象の原因でした。

    対策を講じたコード

    そしてようやく結論です。

    <PanelColorGradientSettings
        title={__("Background Color Setting")}
        settings={[
            {
                colorValue: bg_Color,
                gradientValue: bg_Gradient,
    
                label: __("Choice color or gradient"),
                onColorChange: (newValue) => {
                    setAttributes({ bg_Color: newValue === undefined ? '' : newValue });
                },
                onGradientChange: (newValue) => {
                    setAttributes({ bg_Gradient: newValue });
                },
            }
        ]}
    />
    

    おわかりでしょうか。onColorChangeのコールバック関数のsetAttributesでセットする値をnewValueがundefinedの場合、つまり、選択されなかった場合は空文字にするのです。undefinedでなければ、初期値のセットは行われません。
    そして空文字はfalsy values(falseと評価される値)なのでconst bgColor = bg_Color || bg_Gradient;ではbg_Gradientが選択されるというわけです。

    前にattributesに色を設定するときは、初期値の設定には注意しないといけないというようなことを聞いた記憶があるのですが、その時はその意味も理解せずスルーしていました。
    しかし、こんなしっぺ返しを食らうとは思ってもみませんでした。

  • 「Reactの方法」に沿ったプログラミングとは

    「Reactの方法」に沿ったプログラミングとは

    今や、ReactはJavaScriptの中心的な地位を占めるようになったと感じるときがしばしばあります。TypeScriptのように、さらに新しいプログラミング技法は生まれてきていますが、基本の考え方はReactです。
    それに対して従来のJavaScriptはプログラミングそのものの考え方が違い、別の言語のように感じます。そのためReactを使いながら従前のJavaScriptのようなプログラミングをすると、「それは「Reactの方法」(”The React Way”)に沿ったものではありません。」と言われてしまいます。
    そもそも、”The React Way”とはなんでしょうか。最近、その意味が少しわかった気がしますので、このブログにまとめておきたいと思います。
    なお、従前のJavaScriptのコードはjQueryを導入していることを前提とさせていただきます。変更

    両者の違いの概要

    次の図をご覧ください。

    従前のJavaScriptは、DOM要素があって、それを抽出してその要素が持っているテキストや属性の情報を書き換えて再レンダリングしていました。

    これに対してReactでは、まずDOM要素をレンダリングするための基礎情報というのが存在します。その情報を元にJSXという記法でDOM要素を生成し、レンダリングします。更新するのはDOM要素ではなく、基礎情報の方なのです。JSXというのはHTMLにJavaScriptの変数や条件文を記述できる記法で、HTMLに変数が埋め込まれたような記述になります。

    従前のJavaScriptの記法を”imperative”、日本語でいうと「命令的」といい、Reactを”declarative”、日本語でいうと「宣言的」と言ったりもするようです。

    実際のコードを比較

    実際のプログラミングの場面では”imperative”と”declarative”では、レンダリング結果は同じでも、コードの違いは大きいです。
    ここで誤解がないように説明しますが、Reactでも”imperative”な記法は可能だということです。”imperative”と”declarative”はあくまで記法の問題で、言語の仕様を縛っているものではありません。
    そのため、今回はより違いが明確になるように、Reactで”imperative”なアプローチと”declarative”なアプローチのコードを示します。

    ”imperative”なアプローチ

    まず、”imperative”なコードを示します。

    export function NomalSelect() {
        //オプションの一覧を開く
      const openClick = (event) => {
        const element = event.currentTarget;//⓶
        if (element.classList.contains('open')) {//⓷
          element.classList.remove('open');
        } else {
          element.classList.add('open');
        }
      }
        return (
            <div onClick={openClick}>//⓵
                クリック
            </div>    
        );
    }
    

    ①はJSXでDOM要素にイベントリスナーをセットしています。
    そしてイベントが発生するとopenClickハンドラが発火します。
    ②でeventオブジェクトからクリックされたオブジェクトを取得します。
    ③以下でJavaScriptのaddメソッド、removeメソッドでクラスをつけたり外したりしています。

    このプログラミングは、DOM要素を取得し、それを直接操作して再レンダリングを起こすというまさに”imperative”なアプローチです。

    “declarative”なアプローチ

    次に、”declarative”なコードを示します。

    export function NomalSelect() {
        // open状態を管理するstate
      const [isOpen, setIsOpen] = useState(false);//⓷
        //オプションの一覧を開く
      const openClick = () => {//⓸
        // isOpenの値をトグルする
        setIsOpen(!isOpen);
      }
        return (
            <div 
                onClick={openClick}//⓵
                className={`${isOpen ? 'open' : ''}`}//⓶
            >
                クリック
            </div>    
        );
    }
    

    ①はJSXでDOM要素にイベントリスナーをセットするのはおなじです。
    ②の部分でJSXの記法を使って3項演算子による条件式でクラス名をセットしています。つまり、状態変数isOpenがtrueならクラス名openをつけてfalseなら空のクラスにするという処理です。
    ③は、その判断のための状態変数isOpenをuseStateでセットしています。
    ④以下の処理は状態変数isOpenを更新しています。ここで重要なのはDOM要素を操作しているのではないということです。

    まとめ

    ”imperative”なアプローチと”declarative”なアプローチの違いを、実感していただけたでしょうか?
    一番大きな違いは状態変数isOpenがあるかないかです。これが最初に説明した「基礎情報」です。今回はuseStateで管理する状態変数でしたが、Gutenbergのブロックの場合はブロックの属性(attributes)になることもあります。
    従前のJavaScriptでは、「基礎情報」なしにDOMをレンダリングしてきたので、これに慣れ親しんだ方は”declarative”なアプローチには違和感を感じるのではないかと思います。私もその一人です。
    しかし、”declarative”なアプローチは大きなメリットをもたらします。「基礎情報」の変化によって、再レンダリングが必要なDOM要素が複数あった場合を想像してください。
    従来のJavaScriptでは再レンダリングしなければならないDOM要素を全て抽出し、適切なメソッドを選んで属性等を更新します。
    ReactならJSXの定義で一元的に管理できるのが最大の利点です。それによって「基礎情報」を更新するだけで全てが再レンダリングされるのです。規模の大きなWebサイトでは保守性が格段の差が出てくると思います。
    しかし、何でも”declarative”なアプローチがよいというわけではありません。。特定のユースケース、特にサードパーティのライブラリやフレームワークとの統合においては、直接のDOM操作が必要となる場合もあります。
    そのため、Reactにおいても”imperative”なアプローチの方法は覚えておく必要はあると思います。

  • ブロックの国際化対応は大変な苦難の道のりだった!

    ブロックの国際化対応は大変な苦難の道のりだった!

    WordPressは翻訳関数というのを用意していて、それを使えばpoファイル、moファイルによってタイトルや説明文を多国籍言語で表示させることができます。
    これはWordPressを使うWeb制作者の多くが認識していらっしゃるでしょう。
    もちろん私もそうでした。
    でも、具体的にコーディングしたことはなく、まあその内と思っていたのです。
    ひな型からimport { __ } from '@wordpress/i18n';とライブラリがインポートされているぐらいだから、手軽に使えるものと思い込み、いつでもマスターできると思っていました。
    しかし、とんでもなかったです。3日かかってやっとなんとか翻訳言語が表示できるようになりましたが、とにかく苦労しました。
    このノウハウを決して忘れないようにしたいと思うし、これからチャレンジする方には、少しでも苦労せず身に着けていただけたらいいという思いでブログにします。

    POT、PO、MOの各ファイルの役割

    まず、この図をご覧ください。

    とりあえず、この図で大まかなイメージを掴んでおいてください。

    最初の一歩はPOTファイル

    まず、POTファイルです。
    私は最初POTファイルなるものが何かよくわかりませんでした。いろんな解説記事を見ましたが「POファイルのテンプレート」という表現が多かったです。しかし、これって具体的にイメージしにくいのです。
    そこで、私は次のように表現することにしました。
    「プロジェクトのソースフォルダからプログラムファイルを検索し、その中から()_e()などの翻訳関数を抽出し、その第1引数をリスト化したファイル」
    厳密さは欠けるように思いますが、どんなファイルか具体的なイメージが湧きやすい気がします。

    さらに詳しく説明します。
    翻訳関数は引数を2つとります。
    第1引数は表示する文字列の原文です。普通は英語でしょうね。
    第2引数はテキストドメインです。それってなに?
    今の段階ではその説明はちょっと置いておきましょう。POファイルのところで説明します。

    とにかく、この関数がプラクラム内で使用されていることが前提になります。POTファイルは、その関数の第1引数、つまり、翻訳すべき原文のリストなのです。そして、それに訳文を入力する「枠」がついていますが、POTファイルの段階では、その部分が空なのです。

    なぜ空かというと、それがまさにテンプレートと言われる所以で、そのPOTをもとに日本語訳のついたファイル、中国語訳のついたファイルというように複数のPOファイルを作るためです。

    ということで、POTファイルは国際化対応の根幹となるファイルだと思います。これを確実に作ることから始めるべきだと思いました。
    Poeditなどの便利なアプリケーションが多くのサイトで紹介されえているのですが、このアプリケーションはPOファイルを生成するためのアプリケーションで、POTファイルは別途用意されていることが前提となっています。しかも、POTファイルなくしてプログラムのソースファイルからいきなりPOファイルを生成する機能ももっています。そのため、初心者が最初にこのアプリケーションを使うと、POTファイルの存在価値を意識しないようになってしまう気がします。これはおすすめしません。まず、POTファイルの作り方を覚えましょう。

    WP-CLIのインストール

    WP-CLIはWordPressのよくある作業を管理するための開発者向けのコマンドラインツールです。このツールでPOTファイルを作ります。
    Poeditでも作れますが、少なくともGutenbergのブロック開発環境においては、WP-CLIを使うことは必須だと思います。私は最初PoeditでPOTを生成したため、かなり遠回りをしました。PoeditはPOファイルを作るものでPOTを作るものではないように思います(有料版は試していないのでわかりません。)。

    WP-CLIのインストールは次の手順で簡単にできます。

    インストール手順1(SCOOPのインストール)

    1. PowerShellを管理者権限で開きます。
    2. 以下のコマンドを実行して、Scoopをインストールします:
    Set-ExecutionPolicy RemoteSigned -scope CurrentUser
    iex (new-object net.webclient).downloadstring('https://get.scoop.sh')
    

    インストール手順2(WP-CLIのインストール)

    1. PowerShellを開きます。
    2. 以下のコマンドを実行して、WP-CLIをインストールします:
    scoop install wp-cli
    

    インストールが完了したら、コマンドプロンプトやPowerShellでwp –infoを実行して、正しくインストールされたか確認できます。

    POTファイルの生成

    対象のブロックのルートディレクトリでターミナルを開いて、次のコマンドを実行します。

    wp i18n make-pot ./ languages/itmar_guest_contact_block.pot --exclude=node_modules/*
    

    第1引数./はルートディレクトリ以下のすべてのディレクトリ内のファイルを対象に関数を検索することを意味します。
    第2引数は出力対象のPOTファイル名です。ファイル名は何でもよいのですが、テキストドメイン名を使うのが一般的でしょう。–excludeオプションは、検索対象の中から特定のディレクトリを除外するものです。なくてもよいのですが、ブロックの開発環境には多くの場合node_modulesディレクトリがあり、そこには大量のファイルがあるので、検索対象から外しましょう。

    ということで実際に出来上がったファイルは以下のようになります。

    ・・・
    #: guest-contact-block.php:163
    msgid "Receipt processing completed successfully."
    msgstr ""
    
    #: build/index.js:132
    #: src/edit.js:103
    #: build/index.js:116
    msgid "Inquiry information notification email"
    msgstr ""
    ・・・
    

    このコードはPOTファイルの一部です。msgidが原文の見出しで、msgstrが訳文の見出しです。訳文は空になっていますね。
    #:以下は翻訳関数があったファイルとその行番号です。この情報が非常に重要なのです。これがないと、JSONファイルの作成のところで大きくつまづきます。

    つづいてPOファイルの作成

    ここでPoeditというアプリケーションを使います。
    インストール方法は簡単で、次の公式ページからダウンロードしてそのファイルをダブルクリックするだけです。
    )

    このファイル名は重要です。デフォルトでは「ja.po」となっているので、その前に「テキストドメイン-」と入れます。
    ここでテキストドメインについて説明します。
    テキストドメインは翻訳関数__()等の第2引数に設定すると説明しました。そうすることによって翻訳関数はそのテキストドメインの文字列を含むファイル名を持つファイルから、第1引数にセットした原文の文字列から訳文を検索するようになっているのです。つまり、

    __("Notification email subject", 'itmar_guest_contact_block')
    

    という関数があるとするとitmar_guest_contact_blockという名前を含むファイルを探し、さらに、第1引数の文字列と一致する訳文を探して表示するのです。ですから、ここでつけるファイル名は重要です。これを間違うと訳文は表示されません。

    これで保存すれば無事にPOファイルは出来上がりです。

    MOファイルはなんのためにある?

    ではMOファイルは何のためにあるのでしょう。
    先ほど翻訳関数がテキストドメイン名のついたファイルを探しにいくといいましたが、実際に探しにいくのはPOファイルではなく、MOファイルなのです。そして重要なのはこのMOファイルはPHPの翻訳関数の訳文を表示させるファイルだということです。
    Javascriptの翻訳関数による訳文はMOルがあっても表示されません。

    とりあえず、ここではMOファイルによる訳文の表示に絞って解説していきます。
    MOファイルはPOファイルをバイナリ形式でコンパイルしたファイルで、先ほどPoeditでPOファイルを保存しましたが、そのとき自動的に生成されるようになっています。ただし、これは設定で生成されないようにもできるので、設定されているかどうかは確認しておきましょう。
    Poeditを立ち上げて[ファイル]ー[設定]で次のダイアログが出るので、そこで確認できます。

    load_plugin_textdomainによる読込

    そしてさらにブロックのエントリポイントのPHPファイルに次のように記述しなくてはいけません。

    function itmar_contact_block_block_init() {
        ・・・
        //PHP用のテキストドメインの読込(国際化)
        load_plugin_textdomain( 'itmar_guest_contact_block', false, basename( dirname( __FILE__ ) ) . '/languages' );
    }
    add_action( 'init', 'itmar_contact_block_block_init' );
    

    WordPressのinitアクションフックでload_plugin_textdomain実行するわけです。第1引数はテキストドメイン、第3引数はMOファイルの保存フォルダへの相対パスです。今回はブロックのルートディレクトリ直下のlanguagesフォルダを指しています(第2引数はあまり気にせずfalseでいいようです。)。

    これでPHPで記述された翻訳関数の部分は訳文が表示されます。
    このように自分で任意のフォルダにMOファイルを保存した場合はload_plugin_textdomainで、その場所を指定する必要がありますが、.\wp-content\languages\pluginsというフォルダに保存すれば、load_plugin_textdomainでの指定は必要ありません。
    ただし、このフォルダはプラグインの外にあるフォルダなのでプラグインをインストールしただけでは保存することができず、ユーザーに一手間かけさせることになります。できれば、そうしない方がいいのではないかと思います。

    PHPのコメントヘッダー内の翻訳

    プラグインのエントリポイントのPHPファイルにはコメントヘッダーが付いていて、これがあることでプラグイン名等が表示されます。

    /**
     * Plugin Name:       Guest Contact Block
     * Plugin URI:        https://itmaroon.net
     * Description:       A block with an email submission form.
     * Requires at least: 6.1
     * Requires PHP:      7.0
     * Version:           0.1.0
     * Author:            Web Creator ITmaroon
     * License:           GPL-2.0-or-later
     * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
     * Text Domain:       itmar_guest_contact_block
     * Domain Path:  			/languages
    
     */
    

    こんな感じになっていますが、WP-CLIでPOTファイルを作ると、次のように抽出してくれます。

    #. Plugin Name of the plugin
    msgid "Guest Contact Block"
    msgstr ""
    
    #. Plugin URI of the plugin
    msgid "https://itmaroon.net"
    msgstr ""
    
    #. Description of the plugin
    msgid "A block with an email submission form."
    msgstr ""
    
    #. Author of the plugin
    msgid "Web Creator ITmaroon"
    msgstr ""
    

    この部分については翻訳関数がセットされていなくても、POファイルに訳文を入れてMOファイルを生成するだけで翻訳されます。

    JSONファイルによるJS関数の翻訳

    ここまでの手順も相当複雑でしたがPOT、PO、MOの各ファイルの機能を理解していれば、そんなに苦労せずにたどり着けるのではないかと思います。
    問題はここからなのです。
    なかなか、正確に説明してくれている記事にも巡り合えず、ChatGPTの答えも不正確でした。

    そもそも、PHPの関数とJS(JavaScript)の関数で翻訳の仕組みが違い、しかも、MOファイルではなくJSONファイルを用意しないといけないなんて思いもしませんでした。
    それに気付くのにも時間がかかりました。
    ブロック開発では訳文を表示させたいのは、ほとんどがJSの関数で作られています。それが表示されないと意味がありません。

    それはともかく、コードとしては次のようになっています。

    import { __ } from '@wordpress/i18n';
    ・・・
    return(
        ・・・
        <TextControl
            label={__("Notification email subject", 'itmar_guest_contact_block')}
            ・・・
        />
        ・・・
    )
    

    PHPと違うのは関数のimportが必要であるという点だけです。
    また、JSONファイルを作ること自体も簡単です。プラグインのルートディレクトリで次のコマンドを実行します。

    wp i18n make-json languages/ --no-purge
    

    これでプラグインのルートディレクトリ直下のlanguagesフォルダからpoファイルを探し出してjsonファイルが生成されます。
    これで訳文が表示されるなら簡単なのです。
    しかし、これからが苦難の始まりです。

    wp_set_script_translationsによるJSONファイルの指定(失敗談)

    PHPではload_plugin_textdomainでMOファイルを読み込みましたが、JSONファイルにおいてもそれと同様のプロセスが必要です。

    wp_set_script_translations( $script_handle, 'itmar_guest_contact_block', plugin_dir_path( __FILE__ ) . 'languages' );
    

    このコードをload_plugin_textdomainと同様にinitアクションフックで実行します。
    第2引数がテキストドメインで、第3引数はJSONファイルが保存されているフォルダへの相対パスです。
    問題は第1引数です。これはスクリプトハンドルと呼ばれる文字列です。
    WordPressのテーマでもプラグインでも外部のライブラリを読み込むときはwp_enqueue_scriptというコマンドを使います。このコマンドの第1引数で指定するのがスクリプトハンドルです。wp_enqueue_scriptで指定するのは他のwp_enqueue_scriptで使用するスクリプトハンドルと重複しない任意の文字列でよいのですが、wp_set_script_translationsで使うスクリプトハンドルは、すでにwp_enqueue_script等の登録コマンドで使用されている文字列でないとダメなのです。
    平たく言うと使用実績があるスクリプトハンドルということですね。それがないなら、あらかじめダミーのスクリプト用意してwp_enqueue_script等の登録コマンドを実行しておかなければいけないのです。

    コードとしては次のようになります。

    wp_enqueue_script(
        'itmar_script-handle',
        plugin_dir_url( __FILE__ ) .'dummy.js',
        array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor' ),
        '1.0.0',
        true
    );
    
    wp_set_script_translations( 
        'itmar_script-handle', 
        'itmar_guest_contact_block', plugin_dir_path( __FILE__ ) . 'languages' 
    );
    

    そんな無駄なエンキューしないといけないのかと思うのですが、これでwp_set_script_translationsは機能してくれているはずなのです。

    とおもって、ブロックをリロードして表示を確認しました。
    ・・・英語のままです。なぜ???
    かなり、時間をかけて調べました。すると、JSONファイルのファイル名の形式は
    ${domain}-${locale}-${handle}.jsonまたは${domain}-${locale}-${md5}.jsonと書いてある記事を見つけました。
    WP-CLIが生成したファイル名はitmar_guest_contact_block-ja-bb1d7dea005e67527e728d4801f74b61.jsonで後者の形式です。では、前者の形式にしてみようと思い、次のようにリネームしました。
    itmar_guest_contact_block-ja-itmar_script-handle.json

    これで再度チャレンジ!
    訳文が表示されました!やったー!!
    リネームするのは面倒だけど、これでなんとかなるならこれでいいやと思いました。

    これで他のブロックも同じように国際化対応しようと思い、POファイルを作り、WP-CLIを実行しました。
    すると、さっきとは違ってJSONファイルが複数出来上っています。これってどういうことかほんとに悩みました。

    調べた結果、WP-CLIはPOファイルから翻訳関数があったファイル名を読み取り、その名前をmd5ハッシュに変換してJSONのファイル名にしていました。そのため、POファイルに複数の元ファイル名が記録されていると、その数だけファイルが生成されます。
    こうすることでブラウザで表示されるファイル以外の翻訳ファイルは読みこまずパフォーマンスを向上させる仕組みということもわかりました。
    しかし、これをされると先のリネーム作戦は実行できません。同一フォルダに同一名のファイルは保存できないからです。
    結局、フリダシに戻りました。

    wp_set_script_translationsによるJSONファイルの指定(ようやく成功)

    それから相当色々試してみました。po2jsonというパッケージも試しましたが、今一つしっくりきません。
    その紆余曲折を語ると大変なので、最終的な結論だけ紹介します。
    コードを示します。

    function itmar_contact_block_block_init() {
        $script_handle = 'text_domain_handle';
        // スクリプトの登録
        wp_register_script(
            $script_handle,
            plugins_url( 'build/index.js', __FILE__ ),
            array( 'wp-blocks', 'wp-element', 'wp-i18n', 'wp-block-editor' )
        );
    
        //ブロックの登録
        register_block_type( __DIR__ . '/build',
            array(
                'editor_script' => $script_handle
            )
        );
    
        // その後、このハンドルを使用してスクリプトの翻訳をセット
        wp_set_script_translations( $script_handle, 'itmar_guest_contact_block', plugin_dir_path( __FILE__ ) . 'languages' );
        
        //PHP用のテキストドメインの読込(国際化)
        load_plugin_textdomain( 'itmar_guest_contact_block', false, basename( dirname( __FILE__ ) ) . '/languages' );
    }
    add_action( 'init', 'itmar_contact_block_block_init' );
    

    このコードはこの公式ページを見て考え付きました。
    やっぱり、最後は公式ページですね。
    コードの解説です。
    ①「// スクリプトの登録」のセクションではwp_register_scriptというコマンドを使っています。これは先に紹介したwp_enqueue_scriptと違ってスクリプトファイルをエンキューせず、スクリプトハンドルだけを登録するコマンドです。これでスクリプトハンドルを確保します。
    ②「//ブロックの登録」セクションではregister_block_typeでブロックを登録しますが、その時の登録情報の一つであるeditor_scriptを①で確保したスクリプトハンドルに上書きしています。
    ③「// その後、このハンドルを使用してスクリプトの翻訳をセット」のセクションでは、そのスクリプトハンドルを使って、wp_set_script_translationsを実行しているのです。

    つまり、${domain}-${locale}-${md5}.jsonの形式のファイルが機能するためには、wp_set_script_translationsの第1引数は、ブロックのeditor_scriptに登録されたスクリプトハンドルである必要があるということです。editor_scriptに登録されたスクリプトハンドルというのはbuild/index.jsをロードするものでないといけません。それが上記のコードのwp_register_scriptというわけです。
    @wordpress/create-blockで作ったブロックのプロジェクトではブロックの登録はblock.jsonの情報に基づいて行われるようになっています。その中では"editorScript": "file:./index.js",となっています。wp_register_scriptは、それと同等の働きをするということがわかりました。その上でスクリプトハンドルを使い回すことができるようにするというのが、今回の成功への道のりだったと言えます。

    もう一点忘れていけないのはPOファイルの翻訳関数の存在していたファイル情報にbuild/index.jsが含まれていないければいけないということです。src/edit.jsだけでは表示されません。これはPOTファイルの生成に関連するもので、PoeditでPOTファイルを生成するとうまくいきませんでした。

    POファイルの更新方法

    最後にPOファイルの更新方法を紹介します。これはPoeditの力を借りるのが一番だと思います。
    POファイルの更新というのは、ソースファイルの更新により、翻訳関数の追加、削除、内容の変更が起こったとき必要になります。
    これはPOTファイルを更新する必要があるので、ソースファイルを更新したら、次のWP-CLIコマンドを実行します。

    wp i18n make-pot ./ languages/itmar_guest_contact_block.pot --exclude=node_modules/*
    

    それからPoeditを立ち上げます。

    このように新しい入力枠ができています。ここに入力していくことで更新することができます。

    この作業が終わってPOファイルを保存すればPoeditがMOファイルは更新してくれます。
    しかし、JSONファイルは更新してくれないので、最後に次のコマンドを実行するのを忘れないで下さい。

    wp i18n make-json languages/ --no-purge
    

    長いブログになりましたが、以上にしたいと思います。
    これから国際化対応をする方には、重要な情報を詰め込んだつもりです。お役に立てれば光栄です。
    最後までお読みいただきありがとうございました。

  • 最初の一歩はRichTextの設置

    最初の一歩はRichTextの設置

    ブロックの機能としてテキストを編集することができるというのは、最も基本的な機能だと思います。
    そこで、今回はブロック制作を始められた方向けに、RichTextコンポーネントというWrodpressが用意してくれているコンポーネントの使い方を説明したいと思います。

    この記事の前提事項

    この記事は、npx @wordpress/create-blockでブロックの開発環境が構築できていることを前提としています。wordpress/create-blockでの環境開発の構築方法については、他の記事をご覧ください。
    当ブログでは次のような記事を公開しているので、良かったら参考にしてください。
    同一プラグインで複数ブロックを仕込む方法

    edit.jsのコーディング

    編集画面を描画するためのedit.jsを次のように変更してください。

    import { __ } from '@wordpress/i18n';
    import { useBlockProps, RichText, } from '@wordpress/block-editor';
    import './editor.scss';
    
    export default function Edit(props) {
        const { attributes, setAttributes }=props
        const blockProps = useBlockProps();
        const { content } = attributes;
        const onChangeContent = ( newContent) => {
            setAttributes( {content:newContent} )
        }
        return (
            <div { ...blockProps }>
                <RichText
                    tagName="p"
                    onChange={ onChangeContent }
                    
                    value={ content }
                    placeholder={ __( 'Write your text...' ) }
                />
            </div>
        );
    }
    

    edit.jsをすべて削除して、その後に、コピペして大丈夫です。

    value={ content }
    onChange={ onChangeContent }
    の部分に着目してください。
    この2つでブロックが持つ情報であるattributesの情報を取得・更新しています。上がattributes情報の取得、下が更新です。
    そしてcontentという変数には
    const { content } = attributes;でattributesオブジェクトを分割代入しています。

    block.jsonの編集

    こういうことをできるようにするためには、ブロックにattributesという情報を持たせ、さらにその中にcontentという情報をもたせる’器’を用意してやる必要があります。しかし、現時点でのブロックは、そのような’器’は持っていません。
    この’器’を用意するのがblock.jsonの役割です。
    block.jsonに

    "attributes": {
        "content": {
            "type": "string",
            "source": "html",
            "selector": "p"
        }
    },
    

    と入れてやりましょう。これを入れる場所はどこでもよいのですが、私は習慣的に

    "supports": {
        "html": false
    },
    

    のあとに挿入するようにしています。

    これでブロックが’器’をもちました。

    これでプロジェクトをビルドすれば、ブロックエディタの画面は次のようにテキストを入力できるようになります。

    save.jsの編集

    しかし、編集画面にRichテキストコンポーネントは現れて、文字の入力や編集ができるようになっても、本番のWebサイト(これをフロントエンドという呼び方をします。)には、その文字は表示されません。これを表示するためにはsave.jsを手入れしないといけないのです。
    これはどんなブロックでも同じです。編集画面とフロントエンドは別々に作るということを覚えておいてください。

    save.jsには次のように記述します。

    import { useBlockProps, RichText } from '@wordpress/block-editor';
    export default function save({ attributes }) {
        const { content } = attributes;
        const blockProps = useBlockProps.save();
        return (
            <div { ...blockProps }>
                    <RichText.Content
                        tagName="p"
                        value={ content }		
                    />
                
            </div>
            
        );
    }
    

    これももとのsave.jsをすべて削除して、その後にコピペして大丈夫です。
    こちらは描画するだけなので
    value={ content }
    しか記述がありません。これでプレビュー画面にも描画されます。
    ここで、2点注意事項です。
    1点目はblock.jsonに

    "attributes": {
        "content": {
            "type": "string",
            "source": "html",
            "selector": "p"
        }
    }
    

    と記述しましたが、その中で”source”: “html”の記述があります。これがあるとsave.jsにおいてp要素にcontentの内容を描画するように指定しないとエラーになったり保存されなかったりするということです。ですから、edit.jsの手入れだけしてsave.jsの手入れをしないと編集画面にもcontentの内容が表示されないことになります。
    2点目はsave.jsではRichTextではなく、RichText.Contentを返してやる必要があるということです。これを誤るとエラーを起こします。
    両者の違いをChatGPTに聞いてみると

    RichTextはテキストを編集するためのリッチテキストエリアを提供し、RichText.ContentはRichTextコンポーネントによって編集されたテキストの実際の内容を表し、リッチテキストエリアに表示された通りのテキストを取得します。

    ということでした。
    内容を編集する機能はHTMLの内容を動的に変化させるということです。ここでWordpressの基本を思い出してください。フロントエンドに出力される内容はサーバーで生成されるものです。それとフロントエンドを描画する機能をもつsave.jsが生成するHTMLは違うものであってはいけないのです。当然、フロントエンドで文字の編集ができるようにすることはできません。
    これがフロントエンドの描画(レンダリング)と編集画面のレンダリングの大き違いです。

    RichText機能のカスタマイズ(おまけ)

    最後におまけとしてRichTextの機能を簡単にカスタマイズする方法を紹介しておきます。
    それはブロックコントロールにFormatに関する設定を行うボタンを表示させるものです。

    このようにブロックコントロールには様々なFormatツールが設置されていますが、allowedFormatsに’core/bold’や’core/italic’という文字列を配列で渡すことで制御することができます。

    <RichText
        tagName="p"
        onChange={onChangeContent}
        allowedFormats={['core/bold', 'core/italic', 'core/link']}
        value={content}
        placeholder={__('Write your text...')}
    />
    

    この文字列はRichTextを表示しているブラウザで開発ツールのコンソールを開き、wp.data.select( ‘core/rich-text’ ).getFormatTypes();と入力すると配列が表示されます。

  • インナーブロックの属性を動的に変更するのは簡単じゃなかった!

    インナーブロックの属性を動的に変更するのは簡単じゃなかった!

    Gutenbergのブロック開発をしたことがある方はご経験があると思いますが、ちょっと実用的なブロックを開発しようと思うとインナーブロックを利用することは必須の技術になると思います。そのインナーブロックを制御するのにかなり深いドツボにハマったという経験をご披露したいと思います。

    作成しようとしたブロックの概要

    先日次のようなブロックを作成しました。

    ①、②はインナーブロックで①はAというインナーブロックを、②はB,Cというインナーブロックを持っています。Aは内部にHTMLのinput要素を持っていてユーザーが入力するようになっています。
    Bは静的なラベルです。
    CはHTMLのtable要素で、Gutenbergのcore/tableでレンダリングしています。
    このブロックはユーザーがAに入力した内容をCのテーブルに反映させ、入力内容を確認できるようにするというもので、お問合せフォームなどでよく利用されるものです。上の画像は説明のため①、②を縦に並べましたが、実用段階においては①と②は同時に表示されるのではなく、①のボタンを押すと②が表示され、②のボタンを押すと①に戻るというものです。
    一見単純な操作に見えるのですが、Aの入力にあわせてCを再レンダリングするというのが、実はかなり難しいのです。

    まず、インナーブロックの設置方法から

    まず、インナーブロックの設置方法を説明します。
    これは比較的簡単でuseInnerBlocksPropsというフックを使います。具体的には

    import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
    
    const TEMPLATE = [
        ['core/paragraph', {}],
        ['core/table', {}],
    ];//①
    
    export default function Edit() {
        const blockProps = useBlockProps();
        const innerBlocksProps = useInnerBlocksProps(
            { blockProps },
            {
                template: TEMPLATE,
                templateLock: false
            }
        );//②
    
        return (
            <div {...innerBlocksProps} />//③
        );
    }
    

    これは@wordpress/create-blockで作ったブロックのひな型の中のedit.jsファイルです。

    ①でインナーブロックのテンプレートを作ります。そして②でuseInnerBlocksPropsフックを実行して、③でレンダリングという仕組みです。
    すると、次のようなブロックがブロックエディタ上に現れます。

    テンプレートによる初期化

    先ほどのコードはTEMPLATEにブロックの名称しか入れなかったので、ブロックエディタ上ブロックは初期化されない状態でレンダリングされましたが、TEMPLATEの配列内の要素に初期化するための属性値をオブジェクトとして与えてやれば、初期化することができます。
    コアブロックを初期化するためには、初期化したいブロックに応じたオブジェクトを用意する必要があります。単純なものから複雑なものまで様々です。
    こちらの公式ページに初期化できる属性名が掲載されているので、まずはこのページで調べるのですが、属性名がわかってもそれはオブジェクトのキーの部分がわかるだけで、オブジェクトの値をどのようにするのかがわからないという問題につきあたります。この話をしだすと大きく本題からそれるのでしませんが、今回はcore/paragraphcore/tableの初期化方法だけ説明します。
    core/paragraphは単純でcontent: '初期化しました'というオブジェクトを用意すれば初期化できます。
    core/tableは複雑です。headbodyfootという属性を初期化すればtable要素がレンダリングされるのですが、属性値は単純な文字列ではありません。cellsというキーを持つオブジェクトである必要があります。そこで次のような関数を作りました。

    // セル要素を生成する関数
    const cellObjects = (inputInnerBlocks) => {
        return inputInnerBlocks.map((input_elm) => ({
            cells: [
                {
                    content: input_elm.attributes.labelContent,
                    tag: 'th'
                },
                {
                    content: input_elm.attributes.inputValue,
                    tag: 'td'
                }
            ]
        }));
    }
    

    これを使って次のようにオブジェクトを用意します。

    const tableHead = [];
    const tableBody = cellObjects(inputFigureBlocks);
    const tablefoot = [];
    const tableAttributes = { head: tableHead, body: tableBody, foot: tablefoot };
    

    今回はtableHeadtablefootは空配列にしましたが、ここにも関数で値を入れればHTMLのthead要素とtfoot要素がレンダリングされます。

    ここまでできたら、TEMPLATEにあてはめます。

    const TEMPLATE = [
        ['core/paragraph', { content: '初期化しました' }],
        ['core/table', {...tableAttributes}],
    ];
    

    これでこんなふうに初期化されます。

    ここまでの全コードは次のようになります。

    
    import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
    
    // セル要素を生成する関数
    const cellObjects = (inputInnerBlocks) => {
        return inputInnerBlocks.map((input_elm) => ({
            cells: [
                {
                    content: input_elm.attributes.labelContent,
                    tag: 'th'
                },
                {
                    content: input_elm.attributes.inputValue,
                    tag: 'td'
                }
            ]
        }));
    }
    
    export default function Edit() {
        //とりあえずinputBlocksを静的にハードコードします
        const inputBlocks = [
            { attributes: { labelContent: 'ラベル1', inputValue: 'インプット1' } },
            { attributes: { labelContent: 'ラベル2', inputValue: 'インプット2' } },
        ]
        //テーブルボディを初期化
        const tableBody = cellObjects(inputBlocks);
        const tableAttributes = { body: tableBody };
    
        const TEMPLATE = [
            ['core/paragraph', { content: '初期化しました' }],
            ['core/table', { ...tableAttributes }],
    
        ];
        const blockProps = useBlockProps();
        const innerBlocksProps = useInnerBlocksProps({ blockProps }, {
            template: TEMPLATE,
            templateLock: false
        });
    
        return (
            <div {...innerBlocksProps} />
        );
    }
    

    inputBlocksを外部のブロックから取得する

    上記のコードは、とりあえずinputBlocksを静的にハードコードしましたが、最初の課題に立ち返ると、inputBlocksは外部のブロックのインナーブロックから情報を取得して配列として生成する必要があります。
    そこで、次のようにブロックエディタで別のブロックを用意しました。

    core/groupの中にitmar/design-text-ctrlという自作のカスタムブロックをインナーブロックとして2つ入れました。
    ここで一つ重要な注意点があります。本題からそれてしまいますが、ここでもかなりハマったので書き留めておきます。

    const TEMPLATE1 = [
        ['itmar/design-text-ctrl', {}],
        ['itmar/design-text-ctrl', {}],
    
    ];
    const TEMPLATE2 = [
        ['core/paragraph', { content: '初期化しました' }],
        ['core/table', { ...tableAttributes }],
    
    ];
    
    const innerBlocksProps1 = useInnerBlocksProps({ blockProps }, {
        template: TEMPLATE1,
        templateLock: false
    });
    const innerBlocksProps2 = useInnerBlocksProps({ blockProps }, {
        template: TEMPLATE2,
        templateLock: false
    });
    
    
        return (
            <>
                <div {...innerBlocksProps1} />
                <div {...innerBlocksProps2} />
            </>
        );
    

    こんなふうに書きたくなりませんか?
    このコードでエラーはでません。インナーブロックも2つレンダリングされます。しかし、2種類のテンプレートがレンダリングされることはないのです。こんなふうになります。

    最初にセットした<div {...innerBlocksProps1} />のテンプレートの内容しかレンダリングされません。これを回避する方法はなさそうです。
    したがって、同じブロックの中に複数のインナーブロックコンポーネントを入れることはできないと覚えておくことが重要です。
    ですから、インナーブロックのエリアを複数作りたいのであれば、まったく別のブロックにインナーブロックをいれるという必要があります。

    では、本題に戻ります。
    今回は自分以外のブロックから情報を引き出す必要あります。そこで必要なのがuseSelectフックです。このフックはブロックエディタ内のすべてのブロックの状態を監視してくれます。
    具体的なコードを示します。

    const inputBlocks = useSelect((select) => {
        const {getBlocks} = select('core/block-editor');
        //全ブロック
        const allInnerBlocks = getBlocks();
        //親ブロックを取得
        const parentBlock = allBlocks.find(block => block.name === 'core/group');
        //その中のインナーブロック
        const targetBlocks = parentBlock ? parentBlock.innerBlocks : [];
        return targetBlocks; 
    }, []);
    

    初めてuseSelectを使う方にはわかりにくいと思うので、もう一度この画像で説明します。

    上記のコードでやりたいのは、2つの「A」の状態を監視することです。つまり、まず「A」を抽出しないといけないのです。
    「親ブロック」はブロックエディタ全体、つまり’core/block-editor’です。その中のブロック全部であるallInnerBlocksは①と②です。今回は「A」があるブロックは①であり、その名前はcore/groupであるということがわかっているので、const parentBlock = allBlocks.find(block => block.name === 'core/group');で①をparentBlockとすることができました。あとはその中のインナーブロック全部ということで、const targetBlocks = parentBlock ? parentBlock.innerBlocks : [];でtargetBlocksが2つの「A」ということになります。
    これで「A」の状態が監視できるようになりました。

    ここでまた重要な注意点があります。useSelectは「A」の状態を監視してinputBlocksを書き換えてくれます。しかし、この処理は非同期処理なのです。
    つまり、いつ書き換わるかわからないということです。書き換わった時にインナーブロックのテンプレートが確実に書き換わるようにするためにどうしたらいいかという課題が出てきます。これを解決するのがuseEffectです。
    こんなふうにしてテンプレートを書き換えます。

    const [TEMPLATE, setTemplate] = useState([]);
    
    useEffect(() => {
        //テーブルボディを初期化
        const tableBody = cellObjects(inputBlocks);
        const tableAttributes = { body: tableBody };
        setTemplate(
            [
                ['core/paragraph', { content: '初期化しました' }],
                ['core/table', { ...tableAttributes }],
            ]
        )
    }, [inputBlocks]);
    

    このuseEffectinputBlocksを依存配列に持っているのでuseSelectinputBlocksを書き換えると発火してくれます。それでテンプレートを書き換えるのですが、useEffect内でconst宣言した定数は外にスコープが効かないので、useInnerBlocksPropsから見えません。
    そこで、TEMPLATEを状態変数にしてuseStateで書き換えるのです。こうすればuseInnerBlocksPropsTEMPLATEの状態変化を検知して、インナーブロックを再レンダリングしてくれるはずです。

    テンプレートは動的に書き換わらない!?

    ここまでかなり苦労してインナーブロックの再レンダリングの仕組みを作ってきました。
    しかし、本当の苦難はこれからでした。
    結論から言ってしまいますが、TEMPLATEが書き換わってもインナーブロックの属性は書き換わりません。初期化したときはTEMPLATEの内容にしたがってレンダリングしてくれますが、その後TEMPLATEの内容が書き換わってもその変化には対応してくれません。これはブロックというのはユーザーが手動で書き換えることを前提としたコンポーネントだからでしょう。自動的に書き換えることは、ある意味で御法度なのかもしれません。
    一度初期化したブロックは動的に書き換えることはできないということです。
    ではどうしたらいいのか?
    一旦削除するという方法があるのです。一旦削除すれば、次のレンダリングは初期化から始まるので、新しいテンプレートでレンダリングされます。
    では、どうやって削除するのか。

    削除の方法はuseDispatchというフックでremoveBlocksという関数を取得し、その関数に引数としてブロックのclientIDを渡せば削除されます。
    ただし、単純に削除してテンプレートを書き換えればいいというものではありません。removeBlocksも非同期の関数なのです。つまり、removeBlocksを実行してすぐにテンプレートを書き換えても、ブロックはまだ削除されていないので効果はありません。もう一つuseEffectを用意してremoveBlocksが完了した後に処理が始まるようにしなければなりません。

    整理すると次のようになります。

    1. inputBlocks(画像のAのブロック)の変化
    2. 画像のB,Cブロックの削除
    3. 削除されたことの確認
    4. テンプレートの再生成
    5. インナーブロックの再レンダリング

    具体的には次のようなコードになります。

    import { useSelect, useDispatch } from '@wordpress/data';
    
    export default function Edit() {
    
        //removeBlocks関数の取得
        const { removeBlocks } = useDispatch('core/block-editor');
        //インナーブロックの監視
        const innerBlockIds = useSelect((select) =>
            select('core/block-editor').getBlocks(clientId).map((block) => block.clientId)
        );
        //inputBlocksに変化があればブロックを一旦削除
        useEffect(() => {
            removeBlocks(innerBlockIds);
    
        }, [inputBlocks]);
    
        //ブロックの削除を確認して再度レンダリング
        useEffect(() => {
            if (innerBlockIds.length === 0) {
                //テーブルボディを初期化
                const tableBody = cellObjects(inputBlocks);
                const tableAttributes = { body: tableBody };
                setTemplate(
                    [
                        ['core/paragraph', { content: '初期化しました' }],
                        ['core/table', { ...tableAttributes }],
                    ]
                )
            }
        }, [innerBlockIds.length]);
        
        ・・・
    

    これで、何とかなるんですが、これだけの工程を踏まないといけないというのは、ちょっと大変すぎますよね。

    replaceInnerBlocksという関数が用意されていた

    この存在を最近知りました。この関数を使えば一旦レンダリングされたインナーブロックの差し替えができるのです。removeBlocksで削除して、その完了を待って、再レンダリングというのに比べたらはるかに効率的です。
    その実行部分のコードを示します。

    useEffect(() => {
        //テーブルボディを初期化
        const tableBody = cellObjects(inputBlocks);
        const tableAttributes = { body: tableBody };
        const newInnerBlocks = [
            createBlock('core/paragraph', {}),
            createBlock('core/table', { ...tableAttributes }),
        ];
        replaceInnerBlocks(clientId, newInnerBlocks, false);
    }, [inputBlocks]);
    

    useEffectはこの一本だけです。
    replaceInnerBlocks関数はテンプレートではなく、createBlock関数でブロックをつくって、既存のインナーブロックと差し替えるというものです。したがって、useInnerBlocksPropsで再レンダリングということも行いません。ですから、useInnerBlocksPropsは、次のように最初に初期化していないインナーブロックの枠だけ作るという役割を果たしてくれればいいのです。

    const TEMPLATE = [];
    
    const innerBlocksProps = useInnerBlocksProps({ blockProps }, {
        template: TEMPLATE,
        templateLock: false
    });
    

    TEMPLATEは変化しませんから、状態変数にする必要はなく、そのためのuseStateも必要なくなりました。

    最後に

    長々説明しましたが、最終的な全コードを示します。

    import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
    import { useSelect, useDispatch } from '@wordpress/data';
    import { useEffect } from '@wordpress/element';
    import { createBlock } from '@wordpress/blocks';
    
    // セル要素を生成する関数
    const cellObjects = (inputInnerBlocks) => {
        return inputInnerBlocks.map((input_elm) => ({
            cells: [
                {
                    content: input_elm.attributes.labelContent,
                    tag: 'th'
                },
                {
                    content: input_elm.attributes.inputValue,
                    tag: 'td'
                }
            ]
        }));
    }
    
    export default function Edit({ clientId }) {
    
        //replaceInnerBlocks関数の取得
        const { replaceInnerBlocks } = useDispatch('core/block-editor');
    
        //ブロックエディタ全体('core/block-editor')から'itmar/design-text-ctrl'を抽出
        const inputBlocks = useSelect((select) => {
            const { getBlocks } = select('core/block-editor');
            // 全ブロックを取得
            const allBlocks = getBlocks();
            //親ブロックを取得
            const parentBlock = allBlocks.find(block => block.name === 'core/group');
            //その中のインナーブロック
            const targetBlocks = parentBlock ? parentBlock.innerBlocks : [];
            return targetBlocks;
        }, []); //
    
        //インナーブロックの置き換え
        useEffect(() => {
            //テーブルボディを初期化
            const tableBody = cellObjects(inputBlocks);
            const tableAttributes = { body: tableBody };
            const newInnerBlocks = [
                createBlock('core/paragraph', {}),
                createBlock('core/table', { ...tableAttributes }),
            ];
            replaceInnerBlocks(clientId, newInnerBlocks, false);
        }, [inputBlocks]);
    
    
        const blockProps = useBlockProps();
    
        const TEMPLATE = [];
    
        const innerBlocksProps = useInnerBlocksProps({ blockProps }, {
            template: TEMPLATE,
            templateLock: false
        });
    
        return (
            <>
                <div {...innerBlocksProps} />
            </>
        );
    }
    

    ちょっと実用的なブロックを作成しようとするとインナーブロックの活用は不可欠です。今回はフロントエンドでのレンダリングについては触れませんでしたが、

    <InnerBlocks.Content />
    

    というコンポーネントでレンダリングできるのは、コーディングの作業量を大幅に削減してくれます。
    ですから、インナーブロックは積極的に使っていくべきだと思っています。

    今回の制作作業で是非覚えておきたいことを箇条書きにします

    • インナーブロックはテンプレートで初期化することができる。
    • インナーブロックはテンプレートで複数のブロックを入れることはできるが、テンプレートを分割して複数のエリアに配置することはできない。
    • インナーブロックはテンプレートを差し替えても一度レンダリングした属性は動的に変更されない。
    • 一度レンダリングしたインナーブロックの属性を変更するにはcreateBlock関数でブロックを作成し、それを配列にしてreplaceInnerBlocks関数で差し替える。

    こんな感じでまとめることができると思います。

    いかがだったでしょうか。
    このブログが、これからブロック制作をしようと思っている方の参考になれば幸いです。

  • 文字はなくてもfont-sizeプロパティの設定が役立つことがありますよ

    文字はなくてもfont-sizeプロパティの設定が役立つことがありますよ

    文字がない要素にfont-sizeプロパティを設定するって意味がわからん。
    そう考えがちですが、そんなことはないのです。ふとした思い付きがあったのでご披露させていただきます。

    課題は次のようなUIをGutenbergのブロックで制作するにあたってです。

    フォームでユーザーに入力作業をしてもらっているとき、その進捗がどうなっているのかを表示するプログレスバーです。Bの数字とCのバーはAの文字要素に付加された擬似要素です。Bがbefore要素でCがafter要素です。B要素内には文字があるのでcssでfont-sizeを設定しました。コード全体としては次のようになります。

    &::before {
      content: counter(step);
      counter-increment: step;
      width: 1.5em;
      height: 1.5em;
      line-height: 1.3em;
      text-align: center;
      display: block;
      font-size: ${font_style_num.fontSize};
      font-family: ${font_style_num.fontFamily};
      font-weight: ${font_style_num.fontWeight};
      font-style: ${fontStyle_num};
      color: ${textColor_num};
      background: white;
      border: ${textColor_num} solid 1px;
      border-radius: 50%;
      margin: 0 auto;
    }
    

    ${font_style_num.fontSize}のようにテンプレートリテラルで動的にプロパティ値を設定できるのはCSS in JSの一つである「styled-components」を使っているからです(styled-componentsについては別のブログで解説します。)。
    このようにするとB要素の大きさは動的に変化します。
    ここで問題が生じます。B要素の大きさが変化すると、C要素の縦位置を動かす必要が出てきます(C要素はB要素の中心の高さにないと不細工です。)。
    C要素は次のようなコードで設定されています。

    &::after {
      content: '';
      width: 100%;
      height: 2px;
      position: absolute;
      left: -50%;
      top: 0.75em;
      background-image: linear-gradient(to right, ${textColor_num} 50%, ${bgColor_num} 50%);
      background-position: 0 0;
      background-size: 200% auto;
      transition: all 1s;
      z-index: -1;
      /*put it behind the numbers*/
    }
    

    進捗の変化に応じてアニメーションさせるためにtransitionなどの設定がありますが、それは本題からそれるので無視してください。問題はtop: 0.75em;のところです。emは親要素のフォントサイズを1としたときの相対値で設定する単位です。したがって、親要素であるA要素のフォントサイズが20pxだとすれば、0.75emは15pxになるということですね。
    C要素の縦位置はこれで設定されています。しかし、A要素のフォントサイズはC要素の縦位置とは関係ないんです。関係あるのはB要素の大きさです。この変化をC要素に反映させるのはちょっと複雑です。

    どうしたものかと悩んでいたのですが、ふと目が行ったのはA要素のCSSにある

    width: 1.5em;
    height: 1.5em;
    line-height: 1.3em;
    

    の部分です。これはB要素を〇で囲むためにフォントに外枠を設定している部分ですが、この数値はB要素のフォントサイズの変化に応じて変化します。なぜかというと、この単位emを決定する親要素はA要素のフォントサイズではなく、B要素のフォントサイズだからです。これはB要素のCSS設定にfont-size: ${font_style_num.fontSize};があることの効果です。
    だったら、C要素にも同じように設定してやればいいんじゃないのか。要素に文字がなくてもfont-sizeプロパティは設定できますよね。
    ということで、こんなコードを書きました。

    &::after {
      content: '';
      width: 100%;
      height: 2px;
      position: absolute;
      left: -50%;
      font-size: ${font_style_num.fontSize};//これが入った
      top: 0.75em;
      background-image: linear-gradient(to right, ${textColor_num} 50%, ${bgColor_num} 50%);
      background-position: 0 0;
      background-size: 200% auto;
      transition: all 1s;
      z-index: -1;
      /*put it behind the numbers*/
    }
    

    ばっちりB要素の真ん中にバーが設定されるようになりました。
    A要素からフォントサイズを引っ張ってきて、その単位がpxか emか remかで条件分岐させて計算するしかないかなと思ってうんざりしていたのですが、こうすれば何の苦労もなく設定できました。
    たまにはこんな閃きが浮かぶこともあるんですよ!!

  • オブジェクトの数の数えるにはkeysメソッドを使うって知ってた?

    オブジェクトの数の数えるにはkeysメソッドを使うって知ってた?

    そんなの常識でしょ。と言われちゃうかもしれません。でも、私はハマりました。

    オブジェクトって配列に入っているのと、オブジェクトを含んだオブジェクトと2パターンがありますよね。

    colors=[
        {
          color: '#72aee6',
          name: 'Blue 20'
        },
        {
          color: '#3582c4',
          name: 'Blue 40'
        },
        {
          color: '#e65054',
          name: 'Red 40'
        },
        {
          color: '#8a2424',
          name: 'Red 70'
        },
        {
          color: '#f2d675',
          name: 'Yellow 10'
        },
        {
          color: '#bd8600',
          name: 'Yellow 40'
        }
    ]
    

    これはcolorsという名前の配列の中に、オブジェクトが入っています。いくつオブジェクトを含んでいますか?

    そうこれはcolors.lengthで6が返ってきます。

    これとオブジェクト内のオブジェクトの数を返す方法を同じと考えてはいけません。

    border = { 
        topLeft: "0px", 
        topRight: "0px", 
        bottomRight: "0px", 
        bottomLeft: "0px", 
        value: "0px" 
    }
    

    このborderオブジェクトにはいくつのオブジェクトが含まれていますか。

    ついborder.lengthとやってしまいそうですが、これってundefinedが返ります。

    正しくは

    Object.keys(radius_heading).length
    

    としないといけないのです。

    念のため説明を加えると、Object.keys(radius_heading) というのは、オブジェクトが持っている全てのプロパティ名を配列にするコマンドです。これでプロパティ名が配列になったので、その.length プロパティを使って長さを取得します。それがオブジェクトの数というわけです。

    オブジェクト自体はlengthプロパティは持っていません。案外知らなかったということはありませんか?

  • ラジオボタンのチェックを値で設定するときは配列で設定する!!

    ラジオボタンのチェックを値で設定するときは配列で設定する!!

    ラジオボタンの設定をjqueryのスクリプトで行うときにハマるという話です。

    問題の前提

    ラジオボタンは通常複数の同一name属性をもつ要素が集まってできています。

    <input type="radio" name="post_radio" value="val1" checked>要素1
    <input type="radio" name="post_radio" value="val2">要素2
    <input type="radio" name="post_radio" value="val3">要素3
    

    こんな感じです。
    ここで、要素3にチェックを入れるという設定をjqueryでやりたいという要求があるとします
    まず、

    let elms=$('input:radio[name="post_radio"]')
    

    とやると変数elmsは配列となり、3つの要素が入ります。
    そして、要素3にチェックを入れたいわけです。

    eqメソッドによる設定

    一つの方法としては
    3番目の要素をeqメソッドで特定して、propメソッドでチェックを入れる方法。次のような感じです。

    elms.eq(2).prop('checked',true);
    

    しかし、この方法は「要素3」が3番目にあることがわかっていなければ成立しません。HTMLが書き換わり、要素の数が増えたり、順番が変わるとjqueryも書き換える必要があります。

    値(value属性)で設定する

    そこで、HTMLが変わってもjquery側に影響を及ぼさない方法としてもう一つの方法があります。
    それはvalue属性で要素を特定するという方法です。もちろんこの方法でもvalue属性が書き換わればjqueryも書き換える必要が生じますが、要素の数が変わったり、順番が変わることの頻度に比べると少ないと思われます。また、スクリプトの可読性も高いと思います(何番目という指定より、〇〇というvalueをもつ要素というほうが分かりやすいでしょ。)。
    そこでこんな書き方をします。

      $('input:radio[name="post_radio"]').val(['val3'])
    

    これで要素3にチェックが入るのですが、ここで罠が潜んでいるのです。
    よく見ればわかるのですが、valメソッドの引数が配列になっています。これがこの記事の結論です。

    2つの失敗例

      $('input:radio[name=post_radio]').val('val3')
    

    これだとチェックは入りません。中途半端に覚えているとこのように文字列を入れてしまうと思いませんか?
    このコードはラジオボタンのvalue属性を変更しようとするものです。これを実行すると、チェックの状態は変わりませんが、全てのラジオボタンのvalue属性は’val3’に変わってしまします。

      $('input:radio[name=post_radio]').val(['val3']).prop('checked', true);
    

    これもダメです。これは最後のラジオボタンが選択状態になります。 今回の例はたまたま最後のラジオボタンにチェックを入れようとしているので結果オーライですが2番目に入れようと思って’val2’を指定しても要素3が選択状態になります。これは最初に value=”val2″ のラジオボタンが選択されるのですが、その後で全てのラジオボタンがチェックされようとします。しかし、ラジオボタンのグループに対して、一度に複数の要素をチェックすることはできません。結果的に最後のラジオボタンだけがチェックされた状態になります。

    これ相当ハマりすよ。
    ご注意ください。

  • 同一プラグインで複数ブロックを仕込む方法<WordPressのブロック制作>

    同一プラグインで複数ブロックを仕込む方法<WordPressのブロック制作>

    ブロック開発していくにあたって、プラグインを使うのは一般的です。そのひな型を作成するツールが「wordpress/create-block」で、プラグインフォルダに移動して、ターミナルから
    npx @wordpress/create-block
    と入力します。
    これで、簡単にプラグインのひな型ができるのはいいのですが、プラグイン一つにつき、出来上がるブロックは一つです。
    これではプラグインをインストールする手間がかかって、インストールしてもらえないかもしれません。
    「新しいブロックを作りました。」といって公開しても、それじゃ寂しいですよね。
    だから、最低でも3つか4つはプラグインの中に仕込みたいわけですが、この方法が結構難しいんです。
    ポイントが理解できていないと結構苦労します。
    そこで、今回できるだけ具体的にその方法を説明したいと思います。

    やっぱり最初は@wordpress/create-blockからスタート

    このコマンドが基本となることは間違いありません。

    npx @wordpress/create-block
    

    出来上がったフォルダ構成を変更

    最初は次のようになっています。

    my-block //作成されたプラグインのディレクトリ
    ├── block.json  
    ├── build //ビルドで出力されるファイル(本番環境で使用するファイル)のディレクトリ
    ├── node_modules 
    ├── my-block.php  //PHP 側でブロックを登録するプラグインファイル
    ├── package-lock.json
    ├── package.json 
    ├── readme.txt
    └── src  //開発用ディレクトリ(この中のファイルを編集)
        ├── edit.js  //edit 関数を記述するファイル
        ├── editor.scss  //エディター用スタイル
        ├── index.js  //ブロック用スクリプト(エントリーポイント)
        ├── save.js  //save 関数を記述するファイル
        └── style.scss 
    

    これを次のようにします。

    my-block
    ├── build
    ├── node_modules 
    ├── my-block.php  
    ├── package-lock.json
    ├── package.json 
    ├── readme.txt
    └── src
       └── blocks //新規作成
           └── new-block //新規作成
               ├── block.json //移動  
               ├── edit.js //移動
               ├── editor.scss //移動
               ├── index.js //移動
               ├── save.js //移動
               └── style.scss //移動
    

    お判りでしょうか。srcフォルダの下にblocksフォルダを作り、その下に複数のフォルダを増やしていこうというわけです。この例ではまずnew-blockというフォルダを作りました。

    PHPファイル内のregister_block_typeを書き換え

    PHPファイルには次のようにブロックを登録するPHPの関数が仕込んであり、initというWordPressのアクションフックでこの関数が実行されます。

    function itmar_single_block_init() {
        register_block_type( __DIR__ . '/build' );
    }
    add_action( 'init', 'itmar_single_block_init' );
    

    ここで __DIR__ . '/build'という部分は自分(PHPファイル)がいるディレクトリの中のbuildフォルダを指しています。そして、その中にblock.jsonというファイルがあれば、その内容に基づいてブロックを登録するという機能を果たします。
    これを複数のフォルダに対してその数だけ実行すれば複数のブロックが一度に登録されるという仕組みをつくります。
    どんなふうにコード書くかというと

    function itmar_multi_block_init() {
        foreach (glob(plugin_dir_path(__FILE__) . 'build/blocks/*') as $block) {
            // Static block
            register_block_type($block);
        }
    }
    add_action( 'init', 'itmar_multi_block_init' );
    

    glob(plugin_dir_path(__FILE__) . 'build/blocks/*')でbuild/blocks内のフォルダ名が配列で渡るので、それをループで回してブロック登録を行うというわけです。
    なので、build/blocks内にフォルダを増やしていけば、登録されるブロックは増えていきます。
    ここまで出来たらとりあえずmy-blockフォルダ直下に移動してターミナルから

    npm start
    

    を実行してブロックが登録されていることを確認してください。プラグインを有効にするのを忘れないでくださいね

    @wordpress/create-block –no-pluginでブロックを増やしていく

    ブロックを増やすのは、src/blocksフォルダに移動してターミナルを開き、次のようにコマンドを実行しますbuild/blocksではないですよ。間違わないようにしてください。

    npx @wordpress/create-block --no-plugin
    

    これで会話モードになるのでブロック名などを指定できます。フォルダ名をオプションにしてnpx @wordpress/create-block new-block2 --no-pluginなどとしない方がいいと思います。これだと会話モードにならずname spaceがデフォルトの「create-block」になります。普通は独自のものを使うと思います。特に複数のブロックになると他のブロックと合わせる必要があるのが通常なので、あとで手作業で変更する羽目になります。
    仮にそうなったらblock.jsonだけでなく、editor.scssとstyle.scssのクラス名を変更するのを忘れてはいけません。これを忘れるとスタイルがあたらずハマってしまうことになります。
    これでnpm startが効いていれば複数のブロックが登録されているのを確認できると思います。

    ちなみにこの段階でフォルダ構成は次のようになっています。

    my-block
    ├── build
    ├── node_modules 
    ├── my-block.php  
    ├── package-lock.json
    ├── package.json 
    ├── readme.txt
    └── src
        └── blocks 
            └── new-block 
                ├── block.json  
                ├── edit.js 
                ├── editor.scss 
                ├── index.js
                ├── save.js
                └── style.scss
            └── new-block2 //npx @wordpress/create-block --no-pluginで新たに作成されたフォルダとファイル
                ├── block.json  
                ├── edit.js 
                ├── editor.scss 
                ├── index.js
                ├── save.js
                └── style.scss
    

    私は最初
    このページ
    で勉強させてもらいました。大変助かりました。非常にわかり安いんですが、それでもブロックの登録が確認できずハマったことが何度もありました。でも何回かやっているうちにハマらなくなったんで、初めての方は何度かやるつもりで長い目で見てやりましょう。