オープンソースのWikiであるGROWIにはプラグイン機能が用意されています。自社のデータを表示したり、表示をカスタマイズするのに利用できます。
今回は、GROWIプラグインとしてRSSフィードを一覧表示するプラグインを作りました。ニュースサイトの表示を行ったり、QiitaのGROWIタグを表示したりするのに便利です。最近ではRSSリーダーを利用する方が減っているので、リーダー代わりに最新情報を表示する場として利用できます。
プラグインの動作
Markdownは以下のように記述します。リンクに対して RSS
と記述するのがポイントです。
[RSS](https://qiita.com/tags/growi/feed)
すると、以下のように表示されます。
アドバンス版
RSSフィードをJSONに変換する(CORS対策)ため、RSS to JSON Converter onlineを利用しています。認証なしでも利用できますが、認証することでオプションが幾つか利用できます(並び替え、件数制限など)。
この場合はリンクではなく、Remark Directiveを利用します。{}
内はapiKeyを除いてオプションです。各オプションはスペースで繋ぎます。
::rss[https://qiita.com/tags/growi/feed]{apiKey=API_KEY count=2 order=pubDate}
コードについて
コードはgoofmint/growi-plugin-rss-readerにて公開しています。ライセンスはMIT Licenseになります。
プラグインを追加する
利用する際には、GROWIの管理画面の プラグイン
にて追加してください。URLは https://github.com/goofmint/growi-plugin-rss-reader
です。
注意点
前述の通り、RSSフィードの変換に外部サービスを利用しています。そのため、LAN内のデータは利用できません(ポータルサイトのRSSフィードなど)。また、RSS to JSON Converter onlineの利用制限もあるので、ご注意ください(課金すると制限は解除できます)。
コードについて
今回は a
タグに対する処理(シンプルな使い方)と、Remark Directive の両方をサポートしています。
const activate = (): void => {
if (growiFacade == null || growiFacade.markdownRenderer == null) {
return;
}
const { optionsGenerators } = growiFacade.markdownRenderer;
const originalCustomViewOptions = optionsGenerators.customGenerateViewOptions;
optionsGenerators.customGenerateViewOptions = (...args) => {
const options = originalCustomViewOptions ? originalCustomViewOptions(...args) : optionsGenerators.generateViewOptions(...args);
// シンプル版
const { a } = options.components.a;
options.components.a = rssReader(a);
// Remark Directive版
options.remarkPlugins.push(rssReaderPlugin as any);
return options;
};
// For preview
const originalGeneratePreviewOptions = optionsGenerators.customGeneratePreviewOptions;
optionsGenerators.customGeneratePreviewOptions = (...args) => {
const preview = originalGeneratePreviewOptions ? originalGeneratePreviewOptions(...args) : optionsGenerators.generatePreviewOptions(...args);
const { a } = preview.components;
preview.components.a = rssReader(a);
preview.remarkPlugins.push(rssReaderPlugin as any);
return preview;
};
};
Remark Directive版の処理
Remark Directive版は、渡されたオプションを解析して、 a
タグに変換します。 data-
要素が使えなかったので、 title
要素にデータを渡しています。
つまり、 Remark Directiveでは、シンプル版と同じように a
タグを生成する役割になります。
export const rssReaderPlugin: Plugin = () => {
return (tree: Node) => {
// leafDirective(::)のみを指定
visit(tree, 'leafDirective', (node: Node) => {
const n = node as unknown as GrowiNode;
if (n.name !== 'rss') return;
const data = n.data || (n.data = {});
data.hName = 'a'; // 描画をaタグに変換
data.hChildren = [{ type: 'text', value: 'RSS' }];
const href = n.children[0].url;
data.hProperties = {
href,
title: JSON.stringify(n.attributes),
};
});
};
};
イメージとしては、以下のようになります。
::rss[https://qiita.com/tags/growi/feed]{apiKey=API_KEY count=2 order=pubDate}
というMarkdownが、以下のようなHTMLになっています。
<a
href="https://qiita.com/tags/growi/feed"
title='{"apiKey":"API_KEY","count":2,"order":"pubDate"}'
>
RSS
</a>
コンポーネントとしての処理
そして、シンプル版では、title要素がJSONパースできるかどうかを判定して、処理を行います。
// オプションを解析。エラーなら空で返す
const parseOptions = (str: string) => {
try {
return JSON.parse(str);
}
catch (err) {
return {
apiKey: null,
};
}
};
const API_ENDPOINT = 'https://api.rss2json.com/v1/api.json?';
export const rssReader = (Tag: React.FunctionComponent<any>): React.FunctionComponent<any> => {
return ({
children, title, href, ...props
}) => {
try {
if (children === null || children !== 'RSS') {
return <Tag {...props}>{children}</Tag>;
}
// オプションを解析
const { apiKey, count, order } = parseOptions(title);
// リクエストするURLを生成
const params = new URLSearchParams();
params.append('rss_url', href);
if (apiKey) {
params.append('api_key', apiKey);
params.append('count', count || '10');
params.append('order_by', order || 'pubDate');
}
const url = `${API_ENDPOINT}${params.toString()}`;
// 後述
}
catch (err) {
// console.error(err);
}
// Return the original component if an error occurs
return (
<a {...props}>{children}</a>
);
};
};
データの取得と描画
データの取得は、 react-async
を使って非同期で行います。
// 指定されたURLを呼んで、結果をJSONで返すのみ
const getRss = async({ url }: any) => {
const res = await fetch(url);
const json = await res.json();
return json;
};
データが取得できたら、それを table
タグで描画します。
return (
<Async promiseFn={getRss} url={url}>
{({ data, error, isPending }) => {
if (isPending) return 'Loading...';
if (error) return `Something went wrong: ${error.message}`;
if (data) {
return (
<>
<table className='table table-striped'>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>PubDate</th>
</tr>
</thead>
<tbody>
{(data.items || []).map((item: any) => (
<tr key={item.guid}>
<td><a href={item.link} target='_blank'>{item.title}</a></td>
<td>{item.author}</td>
<td>{item.pubDate}</td>
</tr>
))}
</tbody>
</table>
</>
);
}
return null;
}}
</Async>
);
GROWIコミュニティについて
プラグインの使い方や要望などがあれば、ぜひGROWIコミュニティにお寄せください。実現できそうなものがあれば、なるべく対応します。他にもヘルプチャンネルなどもありますので、ぜひ参加してください!
まとめ
GROWIプラグインを使うと、表示を自由に拡張できます。足りない機能があれば、どんどん追加できます。ぜひ、自分のWikiをカスタマイズしましょう。