DEV Community

Atsushi
Atsushi

Posted on

I made a plugin to display RSS feeds on GROWI

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)
Enter fullscreen mode Exit fullscreen mode

It will be displayed like this:

FireShot Capture 281 - RSS Reader - GROWI - localhost.jpg

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}
Enter fullscreen mode Exit fullscreen mode

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.

Admin

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;
  };
};
Enter fullscreen mode Exit fullscreen mode

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),
      };
    });
  };
};
Enter fullscreen mode Exit fullscreen mode

For example:

::rss[https://qiita.com/tags/growi/feed]{apiKey=API_KEY count=2 order=pubDate}
Enter fullscreen mode Exit fullscreen mode

is converted into the following HTML:

<a
  href="https://qiita.com/tags/growi/feed"
  title='{"apiKey":"API_KEY","count":2,"order":"pubDate"}'
>
  RSS
</a>
Enter fullscreen mode Exit fullscreen mode

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>;
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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!

Join GROWI Slack here

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)