GROWI, an open-source Wiki, provides a plugin feature that can be used to display or customize your data.
This time, I created a plugin for GROWI that displays a list of RSS feeds. It can be handy for displaying news sites or the Qiita GROWI tag. Since fewer people use RSS readers these days, this plugin can serve as a space to display the latest information as a reader replacement.
Plugin Behavior
Markdown is written as follows. The key is to write RSS
in the link.
[RSS](https://qiita.com/tags/growi/feed)
It will be displayed like this:
Advanced Version
To convert RSS feeds to JSON (for CORS handling), I use RSS to JSON Converter online. While it works without authentication, you can enable additional options like sorting and item limits by authenticating.
In this case, instead of a link, I use Remark Directive. Everything in {}
except the apiKey
is optional. Each option is connected with spaces.
::rss[https://qiita.com/tags/growi/feed]{apiKey=API_KEY count=2 order=pubDate}
About the Code
The code is available at goofmint/growi-plugin-rss-reader under the MIT License.
Adding the Plugin
To use it, add it in the Plugin
section of the GROWI admin panel. The URL is https://github.com/goofmint/growi-plugin-rss-reader
.
Notes
As mentioned earlier, an external service is used for converting RSS feeds, so LAN data (e.g., RSS feeds from a portal site) cannot be used. Also, note the usage limitations of RSS to JSON Converter online (these can be lifted with a paid plan).
About the Code
This implementation supports both simple usage for a
tags and 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);
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 Processing
For Remark Directive, it parses the provided options and converts them into an a
tag. Since data-
attributes couldn't be used, the data is passed into the title
attribute.
Thus, the Remark Directive ultimately generates an a
tag, similar to the simple version.
export const rssReaderPlugin: Plugin = () => {
return (tree: Node) => {
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';
data.hChildren = [{ type: 'text', value: 'RSS' }];
const href = n.children[0].url;
data.hProperties = {
href,
title: JSON.stringify(n.attributes),
};
});
};
};
For example:
::rss[https://qiita.com/tags/growi/feed]{apiKey=API_KEY count=2 order=pubDate}
is converted into the following HTML:
<a
href="https://qiita.com/tags/growi/feed"
title='{"apiKey":"API_KEY","count":2,"order":"pubDate"}'
>
RSS
</a>
Component Processing
In the simple version, it parses the title
attribute as JSON and processes it accordingly.
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);
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) {
return <a {...props}>{children}</a>;
}
};
};
Fetching and Rendering Data
Data fetching is handled asynchronously using react-async
.
const getRss = async({ url }: any) => {
const res = await fetch(url);
const json = await res.json();
return json;
};
The retrieved data is rendered in a 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>
);
About the GROWI Community
If you have any questions or requests about using this plugin, feel free to reach out to the GROWI community. If feasible, we’ll try to accommodate your requests. There are also help channels available, so be sure to join!
Conclusion
With GROWI plugins, you can freely extend the display functionality. If there’s a missing feature, feel free to add it. Customize your Wiki as you like!
OSS Development Wiki Tool GROWI | Comfortable Information Sharing for Everyone
Top comments (0)