AlgoliaのInstantsearchをWordPressのカスタムブロックとフロントエンド両方で使えるようにした話

この記事は「Algolia Advent Calendar 2020」12日目の記事です。

先日会社で「Search with Algolia Instansearch Blocks」というプラグインをリリースしました。

その時にブロックエディタとフロントエンド両方でInstantsearchを動くようにしたので、その方法をまとめます。

ビルド系

webpack.config.jsを拡張して、フロントエンド用のJSを別途ビルドします。

const path = require( 'path' );
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );

module.exports = {
    ...defaultConfig,
    entry: {
        index: path.resolve( process.cwd(), 'src', 'index.tsx' ),
        front: path.resolve( process.cwd(), 'src', 'front.tsx' ),
    },
    resolve: {
        ...defaultConfig.resolve,
        extensions: [ '.ts', '.tsx', '.js' ],
    },

src/index.tsxにエディタ向けの実装を、src/front.tsxにフロント向けの実装を定義します。

フロント向けのJSは、renderから

フロント向けのJSでは、Reactをrenderさせるところから行う必要があります。

また、カスタムブロックの性質上、複数個設置される可能性も考慮する必要があります。

以下は雑なサンプルですが、該当するクラス名を持つタグ全てにreact-domrenderを実行しています。

function renderAllIfItemExists<Props = undefined>(elementClassName: string, element: FC<Props>): void {
    const targets = document.querySelectorAll<HTMLElement>(elementClassName)
    if (!targets) return;
    targets.forEach((target) => {
        const props: InstantsearchFrontendProps = {
            appId: target.getAttribute('data-app-id') || undefined,
            apiKey: target.getAttribute('data-searchonly-api-key') || undefined,
        }
        render(createElement(element, props as any), target)
    })
}
renderAllIfItemExists<InstantsearchFrontendProps>('.aib-instantsearch', InstantsearchFrontend)

カスタムブロックの設定情報はdata属性に

Instantsearchでは、AlgoliaのApplication IDやAPI Key、Index nameを指定する必要があります。

これらはWordPressのエディタで制御したいですが、使うのはフロント側となります。

この場合、エディタで保存するHTMLにdata属性で各値を放り込む形をとりました。

...
  save: ({attributes: {
   appId, apiKey
  }}) => (
    <div className="aib-instantsearch" data-app-id={appId} data-searchonly-api-key={apiKey} />
  ),
}

wp_optionsのデータを使う時は、useEntityPropsuseEffectを併用

Application IDやAPI Keyはwp_optionsに保存して、ブロック毎に設定しなくて済むようにできます。

その場合、useEntityPropsを使ってデータを取得し、useEffectsetAttributesを使ってsave側で利用できるように取り回します。

...
  edit: (props) => {
    const  { setAttributes, attributes }  = props;
    const { hitsItems, isUsingPaidPlan, appId, searchOnlyApiKey } = attributes
    const [algoliaSearchOnlyApiKey] = useEntityProp( 'root', 'site', 'aib_algolia_searchonly_api_key' )
    const [algoliaAppId] = useEntityProp( 'root', 'site', 'aib_algolia_app_id' )
    useEffect(() => {
        if (!!algoliaSearchOnlyApiKey && algoliaSearchOnlyApiKey !== searchOnlyApiKey) {
            setAttributes({
                searchOnlyApiKey: algoliaSearchOnlyApiKey
            })
        }
        if (!!algoliaAppId && algoliaAppId !== appId) {
            setAttributes({
                appId: algoliaAppId
            })
        }
    }, [setAttributes, appId, searchOnlyApiKey, algoliaSearchOnlyApiKey, algoliaAppId])
...

Instantsearchはブロックエディタのeditと、フロント向けJSにそれぞれ実装

ブロックエディタに実装することで、記事編集時にもプレビューができます。フロント向けのものを忘れると表になにもでてきません。

今回のケースでは、data属性に値がない時に事故ることを回避するためなどを目的にちょっとややこしいことをしています。


export const AlgoliaInstantSearchWithClient = ({
    searchOnlyAPIKey, appId, indexName, children
}) => {
    const algoliaClient = useMemo(() => {
        if (!appId || !searchOnlyAPIKey) return null;
        return algoliasearch(appId,searchOnlyAPIKey)
    }, [appId, searchOnlyAPIKey])
    const searchClient = useMemo(() => {
        if (!algoliaClient) {
            return {
                search(requests: any) {
                    return Promise.resolve({
                        results: requests.map(() => ({
                          hits: [],
                          nbHits: 0,
                          nbPages: 0,
                          page: 0,
                          processingTimeMS: 0,
                        })),
                    });
                }
            } as any as SearchClient
        }
        return {
            search(requests: any) {
                return algoliaClient.search(requests)
            }
        } as SearchClient
    }, [algoliaClient])
    return (<AlgoliaInstantSearch searchClient={searchClient} indexName={indexName}>{children}</AlgoliaInstantSearch>)
}

app id / api keyが両方揃っていない場合、MockしたAlgoliaクライアントが使用されます。そのため検索機能は何も動きませんが、少なくともこのReactアプリが壊れることはありません。

やってみて

ブロックエディタ上でプレビューする要件を切って、ショートコードとかでInstantsearchを埋め込むように実装した方が正直楽です。

が、Algolia / Gutenbergのいい勉強になったとは思ってます。

Comment