SSGかつソースが公開されている飲食店サイトを見つけたので、プルリクエストを出してみた話

Static Site Generator Advent Calendar 2020」3日目の記事です。

知人からこんなリポジトリを教えてもらいて、面白そうだったので検索機能を実装するプルリクエストを勝手に出してみました。

サイトができた経緯などはNoteで読める様子です

Noteにリニューアルの経緯などがまとめられていました。有料ですが、個人的には結構面白かったので興味がある方はこちらもぜひ。

ミシュランガイドにも掲載されたフレンチレストランのWebサイトをリニューアルしたので、裏側から考え方、実装まで解説します。

(勝手に)やったこと

ソースを見ていると、取り扱っているワインのリストもGitで管理されている様子でした。

「これ、名前とかで検索できたら便利だよなぁ」とふと思った時、ちょうど試したい全文検索のライブラリがあったので、ローカルで遊びがてら作ってみました。

コードを見ると、TypeScriptのトランスパイルを実行したのちにビルドスクリプトが実行されています。

flexsearchは検索インデックスをJSONなどで保存できますので、「ビルド時にインデックスを作成」 -> 「フロントはそのインデックスを使って検索する」という形で実装できそうです。

ということで、ビルドスクリプトにワイン検索のインデックスを作成してJSON出力するタスクを追加しました。

インデックスを作成する処理

// https://github.com/le-benaton/website/blob/master/src/libs/wine-search.ts
    const createIndex = (dataSets: IItem): Index<unknown> => {
        const index = Flexsearch.create({
            tokenize: tokenizer,
            depth: 3,
            doc: {
                id: 'id',
                field: [
                    'title',
                    'ja',
                    'price',
                ]
            }
        })
        dataSets.data.forEach((data, i) => {
            index.add({
                ...data,
                id: i,
            })
        })
        return index
    }

インデックスをJSON出力する処理


    [{
        name: '白ワイン',
        fileName: 'whiteWine',
        index: whiteWineIndex
    }, {
        name: '赤ワイン',
        fileName: 'redWine',
        index: redWineIndex,
     }, {
         name: 'シャンパーニュ',
         fileName: 'champagneWine',
         index: champagneWineIndex,
     }, {
         name: '全体',
         fileName: 'allWine',
         index: allWineIndex
    }].forEach(({name, index, fileName}) => {
        console.log(`Create Wine search index: ${name}`)
        const data = index.export()
        const target = [
            process.cwd(),
            'www',
            `${fileName}.json`
        ].join('/')
        writeFileSync(target, data)
    });

フロントエンドからFlexsearchを実行する

インデックスのJSONファイルは作成できたので、あとはフロントエンドでそのJSONからインデックスを作成し、検索するようなコードを書けばOKです。

/**
 * @see https://www.broadleaves.dev/posts/2019-08-03-gridsome-flexsearch/#%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%82%92%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AB%E5%90%AB%E3%82%81%E3%82%8B
 * @param str
 */
const tokenizer = (str) => {
  if (!str) return [];
  const splitedTexts = str
    // 処理前にアルファベットを小文字に変換
    .toLowerCase()
    // 漢字、カナ、半角英数の連続する塊を切り出し
    // かなと全角英数は対象外
    .match(/[一-龠]+|[ァ-ヴー]+|[a-z0-9]+/g);
  if (!splitedTexts) return [];
  const mappedTexts = splitedTexts
    .filter((word) => word.length > 1)
    // 半角英数の場合、前方一致検索ができるように処理
    .map((word) => {
      if (word.match(/[a-z0-9]+/g)) {
        let token = '';
        return Array.from(word)
          .map((char) => (token += char))
          .filter((token) => token.length > 1);
      } else {
        return word;
      }
    });
  const flatted = mappedTexts.flat();
  return [...new Set(flatted)];
};

/**
 * 取得するファイルのパス
 * @param {*} indexType
 */
function getIndexFilePath(indexType) {
  switch (indexType.toLowerCase()) {
    case 'white':
    case 'whitewine':
      return './whiteWine.json';
    case 'red':
    case 'redwine':
      return './redWine.json';
    case 'champagne':
    case 'champagneWine':
      return './champagneWine.json';
    default:
      return './allWine.json';
  }
}

/**
 * 検索実行
 * @param {string} searchWord
 * @param {string} indexType
 */
function searchWine(searchWord, indexType = 'all') {
  const SearchIndexOption = {
    tokenize: tokenizer,
    depth: 3,
    doc: {
      id: 'id',
      field: ['title', 'ja', 'price'],
    },
  };
  const index = window.FlexSearch.create(SearchIndexOption);
  const renderWineList = document.querySelectorAll(`ul.wine-list-${indexType} > li`);
  fetch(getIndexFilePath(indexType))
    .then((data) => data.json())
    .then((data) => {
      const targetWineIndex = JSON.stringify(data);
      index.import(targetWineIndex);
      const items = index.search(searchWord);
      return items;
    })
    .then((data) => {
      renderWineList.forEach((element) => {
        const wineTitle = element.querySelector('h4').innerText;
        const isDisplay = data.find((wine) => {
          return wine.title === wineTitle;
        });
        element.style.display = isDisplay || data.length === 0 ? 'list-item' : 'none';
      });
    })
    .catch((e) => {
      renderWineList.forEach((element) => {
        element.style.display = 'list-item';
      });
    });
}

その後

機能は実装したけども、フロントのUIまで勝手に作るのはちょっと違うかなと思ったので、ここまででPRを作成してみました。

https://github.com/le-benaton/website/pull/3

結果、静的サイトながらワインの検索機能がついたwebサイトが完成しました。

https://www.benaton.net/

やってみて

「こういう機能あったらもっと便利なのに・・・」と思うサイトに遭遇することは定期的にありますが、Pull Requestを出せるサイトというのは現状かなりレアなのではと思います。ツールやFWのドキュメントサイトくらいですかね。

「良かれと思ってPR作ったのに!」という善意の押し付けのようなトラブルが生まれそうな気はして、「全てのサイトがやるべきだ!」とは言わないですが、こういうことができるのはなかなか面白いなと思いました。

Comment