AI時代にも関わらずフォーラムサービスを作っている話

ここ2週間くらい、粛々と作っているのですが、色々知見もたまってきているので、そのアウトプットとして残しておくことにします。

## コンセプト

サービスのコンセプトとしては、開発者向けのフォーラムサービスになります。複数のコミュニティを1つのサービスで管理します。誰でも新しいコミュニティを作成できて、その中で管理者になったり、逆に他のコミュニティではメンバーとして参加します。

目指す形としては、オープン（誰でも閲覧できる）なDiscordのようなサービスになります。

## なんでフォーラム？

現在、オンラインコミュニティを運営する際に、SlackやDiscordがよく使われています。実際、筆者もコミュニティでSlackやDiscordを使っています。しかし、幾つかの問題があるなと感じています。

### 知見がクローズドになる

チャットはWeb検索できないので、知見がクローズドになりがちです。心理的安全性が高いとも言えますが、同じような質問が定期的に繰り返されたり、初学者がオンラインで知見を得るのが難しくなります。

また、心理的安全性が高いという雰囲気があるために、普段使わないような文言を書いてしまって、スクリーンショットを撮られてしまうなんてこともあります。

### 無料で使い続けられるか分からない

SlackはSalesforceに買収されて以降、何回かの機能改定を行っています。過去の履歴もしばらく経つと消えてしまいます。知見の積み重ねが難しく、その意味でコミュニティには向いていません。

Discordは個人的には好きなのですが、UXが若干変わっているのか、初心者には難しいと受け止められてしまうことが多いです。Discord自体は無料で使えますが（個々人で有料化可能）、今後も維持されるかは分かりません。

### アカウント管理が煩雑に

これはSlackの場合ですが、URL毎にアカウントが分かれるのが個人的に好きではありません。アカウントが増えれば、それだけパスワードの管理が煩雑になったり、人によっては使い回したりします。URL毎にキャラを変えられるという利点もありますが、一つのアカウントで管理できるメリットの方が大きいかなと感じます。設定も独立していて、その度に設定をしないといけないのも面倒です。

## フォーラムの利点

フォーラムの利点としては、以下のような点があると考えています。

### オープン性

フォーラムはWeb検索でき、知見がオープンになります。オープンな場でのコミュニケーションは、適切な振る舞いを促します。

### ユーザーの自由度

フォーラムは単なる閲覧だけであれば、ユーザー登録不要で可能です。チャットのように、清水の舞台から飛び降りる覚悟はいりません。

## 既存のフォーラムの課題

日本の開発者が多いフォーラムサービスというのは、あまり多くないかと思います。

- 5ちゃんねる  
匿名性というメリット&デメリット
- Reddit  
日本語のサブレディットもあるが、基本的は英語向け
- スラド  
サ終…
- スタック・オーバーフロー  
QA特化型。タグはあるけれど、コミュニティという形ではない。
- Teratail  
QA特化型。タグはあるけれど、コミュニティという形ではない。

オープンソースのフォーラムソフトウェアもありますが、基本的に1コミュニティ、1フォーラムです。つまり、自分たちのコミュニティとして、自分たちでサーバーを立ち上げる必要があります。となると、コミュニティのためのサーバー運用が面倒だったり、コミュニティ毎にアカウントを登録する必要があります。

- [Multiple Community? \| NodeBB Community](https://community.nodebb.org/topic/15469/multiple-community)
- [Multiple communities on discourse? \- Support \- Discourse Meta](https://meta.discourse.org/t/multiple-communities-on-discourse/209392)

## フォーラムを作り始める

ということで、フォーラムを作り始めたのですが。スクラッチで開発するのは大変ですし、車輪の再発明が多いです。そこで、以下のようなオープンソースのフォーラムを調査してみました。

- Discourse
- NodeBB
- Flarum
- phpBB

その結果、NodeBBでプラグインを作れば、ある程度のことはできそうだと分かりました。オープンソースなら、サ終時にも安心（？）です。

## NodeBBのプラグインの仕組み

NodeBBのプラグインは、以下の3つのファイルで構成されます。

- plugin.json  
プラグインの基本情報
- package.json  
プラグインの依存関係や、パッケージとしてインストール用
- library.js  
プラグインのコード（ファイル名は任意）

### プラグインを作る

`plugin.json`は、プラグインの基本情報を記述します。

```json
{
  "id": "nodebb-plugin-caiz",
  "name": "NodeBB Plugin for Caiz",
  "description": "NodeBB Plugin for Caiz",
  "version": "1.0.0",
  "library": "./library.js",
  "staticDirs": {
    "language": "locales"
  },
  "hooks": [
    {
      "hook": "static:app.load",
      "method": "init"
    },
    {
      "hook": "filter:categories.build",
      "method": "customizeCategoriesLink"
    },
    {
      "hook": "filter:topic.build",
      "method": "customizeTopicRender"
    },
    {
      "hook": "filter:category.get",
      "method": "customizeCategoryLink"
    }
  ]
}
```

基本的に `hooks` 以下に、プラグインの呼び出すイベントとJavaScriptの関数名を記述します。

[Hooks \- NodeBB Documentation](https://docs.nodebb.org/development/plugins/hooks/)

現時点で使っているフックは2種類で、サーバーサイドフックは[Hooks · NodeBB/NodeBB Wiki](https://github.com/NodeBB/NodeBB/wiki/Hooks/)にて一覧が定義されています。もう一つはテンプレートのレンダリング時に呼ばれるビルドフックで、これは `filter:(テンプレート名).build` という形式で呼び出されます。ただ、ビルドフックはファイル名を指定しても呼ばれないことがあって、どういう仕組みなのか完全には把握できていません…。

### プラグインのコードを作る

プラグインの基本形です。 `hooks` の `static:app.load` で `init` を指定しているので、 `plugin.init` が呼び出されます。これはアプリの起動時に呼び出されるフック関数です。

```js
'use strict';
const winston = require.main.require('winston'); 
const plugin = {};
plugin.init = async function (params) {
  const { router, middleware, controllers } = params;
  winston.info('[plugin/caiz] Initializing Caiz plugin');

  router.get('/api/communities', controllers.categories.list);
  router.get('/api/:handle', Community.Index);
};
module.exports = plugin;
```

ログは `console.log` でも出力できますが、 `winston` を使うとログのフォーマットを変えたり、ログの出力先を変えたりできます。

ルーティングの `router` は `Express` なので、 `router.get` などでルーティングを追加できます。

`controllers` は `NodeBB` のコントローラーで、 [`src/controllers`](https://github.com/NodeBB/NodeBB/blob/master/src/controllers/index.js) とマッピングされているので分かりやすいです。

### テンプレートをカスタマイズする

NodeBBのデフォルトテンプレートは[NodeBB/nodebb\-theme\-harmony](https://github.com/nodebb/nodebb-theme-harmony)です。これをクローンして、テーマとして指定し、カスタマイズします。

表示はBootstrapを使っていて、テンプレートエンジンは[benchpressjs](https://github.com/benchpressjs/benchpressjs)を使っています。表示内容をカスタマイズ際には、 `build` フックを使って、 `templateData` を変更します。

```js
static async customizeIndexLink(data) {
  const { categories } = data.templateData;
  categories.forEach(category => {
    category.link = `/${category.handle}`;
    if (!category.children) return;
    category.children.forEach(child => {
      child.link = `/${category.handle}/${child.cid}-${child.handle}`;
    });
  });
  return data;
}
```

こんなコードで書くと、テンプレート側では `{ ./link }` で表示できます。テンプレートは `templates` ディレクトリにある `*.tpl` ファイルです。なお、VS CodeなどではBenchpressの表示サポートがなかったので、HTMLファイルとしてハイライトするのが良さそうです。

## テーマ、プラグインの反映

テーマやプラグインは、npmコマンドでインストールします。

```bash
npm install nodebb-theme-caiz
```

これで `nodebb-theme-caiz` がインストールされます。インストール後、アクティブにします。

```bash
./nodebb activate nodebb-theme-caiz
```

これでテーマやプラグインがアクティブになります。

### 修正した場合

プラグインを修正したら、NodeBBを再起動します。これはコマンド、または管理画面から行えます。

```bash
./nodebb restart
```

テーマを修正したら、ビルドも必要です。これもコマンド、または管理画面から行えます。

```bash
./nodebb build
./nodebb restart
```

Docker Composeで開発している場合にはリスタートがうまくいかなかったので、管理画面から操作するのが良さそうです。

## 現状

現状はまだプラグインとテーマの機構が分かり始めた段階で、色々と足りていない機能が多いです。

- コミュニティの作成・編集・削除
- メンバーの登録・削除
- メンバーの権限登録・編集・削除
- AIを使った機能

投稿などの機能自体はNodeBB標準のものが使えると思います。作りつつ、知見があれば公開していきます。というか、NodeBBのプラグイン開発に関する公開情報が少なく、手探り状態です。なので、自分用メモとして残していきます。Qiitaですら、NodeBBタグはないのが、その証左…といったところでしょうか。

さらに言えば、情報が多くないので、AIコーディングエージェントを使っても、動かないコードを生成するケースがとても多いです。こうやって知見を公開すれば、徐々に賢くなっていく…かも知れません。

## まとめ

NodeBBのフックは種類がとても多く、さまざまな機能をプラグインのみで実装できそうです。設定なども多いので、プラグインと設定、テーマを使いこなせば、大抵のニーズは満たせそうな予感がします。

コードは以下にて公開しています。

[goofmint/caiz](https://github.com/goofmint/caiz)