Plugins
A plugin is a module that knows how to fetch and format a specific type of source. For example, the Reddit plugin understands the Reddit API and formats posts for the feed UI.
Plugins are responsible for:
- Authenticating to access private data sources
- Parsing HTML/JSON
- Formatting content into the format OpenFeed expects
Without a plugin, OpenFeed will attempt to scrape (or embed) the entire URL of the page.
Plugins are matched using a canHandle(url) function. The first plugin that claims a URL wins. OpenFeed tries specific plugins before generic fallbacks like the RSS plugin.
Supported plugins
The following plugins ship as part of OpenFeed and do not need to be installed:
| Plugin | Handles | Auth required |
|---|---|---|
youtube-rss | YouTube channel and video URLs | No |
reddit | Reddit subreddit and post URLs | No |
substack-rss | Substack publication URLs | No |
buttondown | Buttondown newsletter URLs | No |
bluesky | Bluesky profile URLs | No |
hacker-news | Hacker News | No |
dev-to | dev.to articles | No |
medium | Medium publication URLs | No |
rss | Any RSS or Atom feed URL | No |
podcasts | Podcast RSS feed URLs | No |
open-meteo | Open-Meteo weather API URLs | No |
github | GitHub repo and user URLs | No |
cnn | CNN articles | No |
washington-post | Washington Post articles | No |
wall-street-journal | WSJ articles | No |
politico | Politico articles | No |
associated-press | AP News articles | No |
reuters | Reuters articles | No |
al-jazeera | Al Jazeera articles | No |
the-new-yorker | New Yorker articles | No |
espn | ESPN articles | No |
centcom | CENTCOM press releases | No |
nyt-crossword | NYT Crossword | No |
wordle | Wordle daily puzzle | No |
tiktok | TikTok profile URLs | No |
instagram-firecrawl | Instagram profile URLs | FIRECRAWL_API_KEY |
google-calendar | Google Calendar feed URLs | OAuth token |
gmail | Gmail feed URLs | OAuth token |
If no plugin matches a URL, OpenFeed falls back to the default plugin which skips the source with a warning.
Installing plugins
You can install additional plugins from npm:
npm install open-feed-plugin-exampleThen register the plugin in your open-feed.yaml:
plugins:
- open-feed-plugin-exampleConfiguring plugins
Some plugins accept an options object in your source config:
sources:
- name: My Source
url: https://example.com/
options:
limit: 10
includeImages: trueRefer to individual plugin documentation for supported options.
Writing your own plugin
You can create and share your own plugins to pull custom sources into OpenFeed.
Basics
An OpenFeed plugin is a TypeScript module that exports a BackendFeedPlugin object with the following shape:
import type { BackendFeedPlugin, FetchFn, NewFeedItem } from 'open-feed/plugins/types'
const myPlugin: BackendFeedPlugin = {
name: 'my-plugin',
icon: `<svg>...</svg>`, // optional SVG icon string
// Return true if this plugin can handle the given URL
canHandle: (url: string) => url.includes('mysite.com'),
// Fetch and return items for the given source URL
listItems: async (
sourceUrl: string,
fetchFn: FetchFn,
options?: Record<string, unknown>
): Promise<readonly NewFeedItem[]> => {
const response = await fetchFn(sourceUrl)
const data = await response.json()
return data.items.map(item => ({
sourceName: 'My Source',
sourceUrl,
title: item.title,
url: item.url,
publishedAt: new Date(item.date),
renderData: {
richText: { text: item.body }
}
}))
}
}
export default myPluginEach item must include at least one render method in renderData:
| Method | Data | How it renders |
|---|---|---|
video | { videoId?, url? } | YouTube embed or <video> |
richText | { html?, text } | Formatted article content |
audio | { url } | <audio> player |
embed | { url } | <iframe> fallback |
Local testing
Write tests using Vitest. Inject a mock fetchFn — never hit the network in tests:
import { describe, it, expect, vi } from 'vitest'
import myPlugin from './index'
describe('myPlugin', () => {
it('handles mysite.com URLs', () => {
expect(myPlugin.canHandle('https://mysite.com/feed')).toBe(true)
expect(myPlugin.canHandle('https://other.com')).toBe(false)
})
it('returns items', async () => {
const mockFetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({
items: [{ title: 'Test', url: 'https://mysite.com/1', date: '2024-01-01', body: 'Hello' }]
})
})
const items = await myPlugin.listItems('https://mysite.com', mockFetch)
expect(items).toHaveLength(1)
expect(items[0].title).toBe('Test')
})
})Publish
Publish your plugin to npm with the open-feed-plugin- prefix so others can discover it:
npm publish --access publicWe encourage you to share your plugins by opening a PR to add them to the built-in plugin list.