# Starred News Sync Import starred or saved RSS reader items into Obsidian as Markdown notes with YAML frontmatter. [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.18341648-blue)](https://doi.org/10.5281/zenodo.20370046) ## Features - Adds a **Sync starred items now** command and ribbon action. - Imports each item once using an Obsidian-safe filename in the form `Article title - RSS shortHash.md`. - Creates notes with YAML fields for title, URL, reader, feed, author, published date, import date, and tags. - Supports manual sync and optional interval sync while Obsidian is open. - Converts returned HTML summaries/content into Markdown and strips unsafe HTML elements and event attributes. - Optionally fetches the original article page for new imports and extracts readable content with Defuddle. - Keeps fetched article images only when remote images are explicitly enabled. - Can render imported notes from an optional Templater-compatible template. - Can skip items whose URL already exists in configurable YAML frontmatter fields. ## Supported readers | Reader | Plugin provider | Setup notes | | --- | --- | --- | | FreshRSS | Google Reader-compatible or Fever-compatible | Use the API URLs from FreshRSS settings, usually `/api/greader.php` or `/api/fever.php`, with the FreshRSS API password. | | Tiny Tiny RSS | Tiny Tiny RSS | Enable the JSON API and use the `/api/` endpoint. Starred items are imported from the special starred feed. | | Inoreader | Inoreader | Use `https://www.inoreader.com`. Paste an OAuth bearer token when possible. The plugin can also use legacy ClientLogin with username/password and optional App ID/App Key, but it does not run the browser OAuth redirect flow for you. | | Feedly | Feedly | Requires a Feedly API bearer token and a stream ID. For Feedly Teams this is usually a board, folder, or AI Feed stream ID. | | Miniflux | Miniflux | Use a Miniflux API token. The plugin fetches `/v1/entries?starred=true`. Miniflux can also be used through its Fever API. | Other likely compatible readers include services or servers that expose Google Reader-compatible or Fever-compatible APIs, such as CommaFeed, FeedHQ, The Old Reader, and BazQux. Compatibility depends on the exact API variant and authentication requirements. ## Settings - **Reader API**: Select the API type your reader exposes. - **API URL**: The root endpoint for that API. Examples: - FreshRSS Google Reader: `https://rss.example.com/api/greader.php` - FreshRSS Fever: `https://rss.example.com/api/fever.php` - Tiny Tiny RSS: `https://rss.example.com/tt-rss/api/` - Miniflux: `https://rss.example.com` - **Username** and **Password or API password**: Used by Google Reader-compatible, Fever, Tiny Tiny RSS, and legacy Inoreader flows. - **Access token**: Used by Feedly, Miniflux, OAuth-based Inoreader, or precomputed GoogleLogin/Fever tokens. - **Inoreader app identifier** and **Inoreader app key**: Optional. Only used for legacy Inoreader ClientLogin. Leave them blank when using an OAuth bearer token. - **Output folder**: Destination folder in the vault. - **Note tags**: Comma-separated YAML tags added to imported notes. - **Skip duplicate links**: Optional. Skips reader items whose URL already appears in configured YAML frontmatter fields inside the output folder. - **Duplicate URL property**: YAML frontmatter property compared with incoming item URLs. Defaults to `url`; comma-separated legacy names are supported. - **Note template**: Optional vault path to a Markdown template. If Templater is installed, Templater commands are rendered with the imported RSS item injected as `rss`. - **Include debug view**: Optional. Appends a collapsed debug section to each imported note showing either the normalized incoming item, the raw per-item reader payload, or a simple field list. - **Enable debug logging**: Optional. Writes per-item import and article-fetch decisions to the developer console so you can see why an item was skipped, fetched, or left unchanged. - **Fetch article source text**: Optional. Requests each article page for new imports, extracts readable content, and records `content_source` and `content_fetched_at` in frontmatter. - **Source fetch mode**: Choose whether article pages are fetched only when reader content is missing or blank, or always preferred over reader content. - **Include remote images**: Optional. Keeps safe HTTP and HTTPS image links from fetched article pages. Off by default because previewing notes may contact image hosts. - **Sync on startup**: Optional. Runs one sync after Obsidian opens and the workspace is ready. - **Automatic sync**: Runs sync on an interval while Obsidian is open. Credentials are stored in this plugin's Obsidian data file. By default, the plugin only sends network requests to the reader API URL you configure. ### Inoreader setup Set **Reader API** to **Inoreader** and **API URL** to `https://www.inoreader.com` unless you are using a proxy. Preferred setup, using OAuth bearer tokens: 1. Register an app in Inoreader under **Preferences -> Other -> Create new application**. 2. Obtain an access token outside the plugin. The plugin accepts the finished bearer token, but it does not open the consent page, receive the redirect callback, exchange the authorization code with a POST request, or refresh expired tokens. 3. If you do not want to build your own callback endpoint, Inoreader documents a working manual path with the Google OAuth 2.0 Playground: - Authorization endpoint: `https://www.inoreader.com/oauth2/auth?state=test` - Token endpoint: `https://www.inoreader.com/oauth2/token` - OAuth flow: Server-side - Scope: `read` 4. After the playground exchanges the authorization code, paste the returned `access_token` into **Access token** in this plugin. 5. Leave **Username**, **Password or API password**, **Inoreader app identifier**, and **Inoreader app key** blank when using a bearer token. Legacy setup, using ClientLogin: 1. Fill in **Username** and **Password or API password**. 2. Add **Inoreader app identifier** and **Inoreader app key** only if your account or app registration requires Inoreader app authentication for ClientLogin requests. 3. This path uses Inoreader's older `GoogleLogin auth=...` flow, not OAuth 2.0. If Inoreader returns `403`, the usual causes are invalid or missing app credentials for the legacy ClientLogin path, API access restrictions on the Inoreader account or app, or an expired or invalid OAuth token. The ` - RSS ` filename suffix is still used for newly imported notes so items with the same title but different URLs do not target the same file path. The plugin also stores the same stable hash as `rss_hash` in imported note frontmatter and recognizes the legacy filename suffix for older imports. Existing imports are skipped when that hash is found in the configured **Output folder** and its subfolders, so you can rename imported notes as long as `rss_hash` remains in the note frontmatter. When **Skip duplicate links** is enabled, the plugin also scans Markdown frontmatter in that folder tree for the configured URL property names before importing. URLs are compared after light normalization: surrounding whitespace and URL fragments are ignored, hostnames are compared case-insensitively, default HTTP/HTTPS ports are ignored, and trailing path slashes are ignored. Query strings are preserved. For reliable deduplication, keep these frontmatter properties in imported notes: - `rss_hash`: Required for rename-safe duplicate detection. The plugin adds this automatically, including to custom template output when it is missing. - `url`: Required only when **Skip duplicate links** is enabled with the default duplicate URL property. If you configure different duplicate URL properties, keep those properties instead. Existing notes imported before `rss_hash` was added can still be detected by the legacy ` - RSS .md` filename suffix until they are renamed. If you rename those older notes, add `rss_hash` first. If **Fetch article source text** is enabled, the plugin also requests article URLs from your starred items and extracts readable content with [Defuddle](https://github.com/kepano/defuddle). It only accepts HTTP and HTTPS URLs, skips localhost/private-network-style hosts, limits article responses to 2 MB, removes scripts/forms/active content/inline event handlers, disables Defuddle's async third-party fallbacks, and converts the extracted HTML to Markdown before writing notes. Remote article images are removed unless **Include remote images** is enabled; when enabled, only safe HTTP and HTTPS image links are kept, and Obsidian may contact those image hosts when rendering notes. This may still disclose your IP address and user agent to article websites. If you want local copies of remote images, consider using an attachment-localizing Obsidian plugin such as [Local Images Plus](https://community.obsidian.md/plugins/obsidian-local-images-plus). ## Templates Set **Note template** to a vault path such as `Templates/Starred news item`. Wikilinks such as `[[Templates/Starred news item]]` also work. When [Templater](https://github.com/SilentVoid13/Templater) is installed, the plugin injects these objects before rendering: - `rss`: the full import context. - `item`: alias for `rss.item`, containing normalized reader fields. - `content`: alias for `rss.content`, containing selected HTML/Markdown content fields. Common fields include `rss.title`, `rss.url`, `rss.reader`, `rss.author`, `rss.feedTitle`, `rss.feedUrl`, `rss.publishedAt`, `rss.updatedAt`, `rss.importedAt`, `rss.tags`, `rss.notePath`, `rss.fileName`, `rss.shortHash`, `rss.byline`, `rss.contentHtml`, `rss.summaryHtml`, `rss.selectedContentHtml`, `rss.contentMarkdown`, `rss.summaryMarkdown`, `rss.contentSource`, `rss.contentFetchedAt`, and `rss.rawApiItem`. The default YAML fields are also available as `rss.frontmatter`. `rss.rawApiItem` contains the preserved per-item payload returned by the reader before local enrichment such as article fetching. Its shape is provider-specific and may vary across reader servers, so treat it as debug data rather than a stable cross-provider template contract. Sample `rss` object with representative values: ```json { "id": "tag:example.com,2026:starred/12345", "title": "Shipping small plugins without breaking your vault", "url": "https://example.com/posts/shipping-small-plugins", "reader": "miniflux", "author": "Casey Example", "feedTitle": "Example Engineering", "feedUrl": "https://example.com/feed.xml", "publishedAt": "2026-05-24T08:30:00.000Z", "updatedAt": "2026-05-24T09:10:00.000Z", "contentHtml": "

Full article body.

It has several paragraphs.

", "summaryHtml": "

Short reader summary.

", "contentSource": "article_url", "contentFetchedAt": "2026-05-30T11:42:13.000Z", "rawApiItem": { "id": "tag:example.com,2026:starred/12345", "title": "Shipping small plugins without breaking your vault", "canonical": [ { "href": "https://example.com/posts/shipping-small-plugins" } ], "origin": { "title": "Example Engineering", "htmlUrl": "https://example.com/feed.xml" }, "content": { "content": "

Full article body.

It has several paragraphs.

" } }, "importedAt": "2026-05-30T11:42:14.000Z", "tags": ["rss", "starred"], "notePath": "News/Shipping small plugins without breaking your vault - RSS a1b2c3d4.md", "fileName": "Shipping small plugins without breaking your vault - RSS a1b2c3d4.md", "shortHash": "a1b2c3d4", "byline": "Example Engineering | Casey Example | 2026-05-24", "selectedContentHtml": "

Full article body.

It has several paragraphs.

", "contentMarkdown": "Full article body.\n\nIt has several paragraphs.", "summaryMarkdown": "Short reader summary.", "content": { "html": "

Full article body.

It has several paragraphs.

", "markdown": "Full article body.\n\nIt has several paragraphs.", "readerHtml": "

Full article body.

It has several paragraphs.

", "summaryHtml": "

Short reader summary.

", "summaryMarkdown": "Short reader summary.", "source": "article_url", "fetchedAt": "2026-05-30T11:42:13.000Z" }, "item": { "id": "tag:example.com,2026:starred/12345", "title": "Shipping small plugins without breaking your vault", "url": "https://example.com/posts/shipping-small-plugins", "reader": "miniflux", "author": "Casey Example", "feedTitle": "Example Engineering", "feedUrl": "https://example.com/feed.xml", "publishedAt": "2026-05-24T08:30:00.000Z", "updatedAt": "2026-05-24T09:10:00.000Z", "contentHtml": "

Full article body.

It has several paragraphs.

", "summaryHtml": "

Short reader summary.

", "contentSource": "article_url", "contentFetchedAt": "2026-05-30T11:42:13.000Z" }, "frontmatter": { "title": "Shipping small plugins without breaking your vault", "url": "https://example.com/posts/shipping-small-plugins", "reader": "miniflux", "reader_item_id": "tag:example.com,2026:starred/12345", "imported": "2026-05-30T11:42:14.000Z", "rss_hash": "a1b2c3d4", "author": "Casey Example", "feed": "Example Engineering", "feed_url": "https://example.com/feed.xml", "published": "2026-05-24T08:30:00.000Z", "updated": "2026-05-24T09:10:00.000Z", "content_source": "article_url", "content_fetched_at": "2026-05-30T11:42:13.000Z", "tags": ["rss", "starred"] } } ``` Using the sample object above, these template markers render to these values: - `{{rss.title}}` -> `Shipping small plugins without breaking your vault` - `{{rss.summaryMarkdown}}` -> `Short reader summary.` - `{{rss.contentMarkdown}}` -> `Full article body.` followed by `It has several paragraphs.` - `{{item.feedTitle}}` -> `Example Engineering` - `{{content.markdown}}` -> same value as `{{rss.contentMarkdown}}` - `{{content.summaryMarkdown}}` -> same value as `{{rss.summaryMarkdown}}` - `{{rss.rawApiItem}}` -> pretty-printed JSON of the preserved provider item payload `summaryMarkdown` and `contentMarkdown` are intentionally different fields. `summaryMarkdown` always comes from the reader-provided summary. `contentMarkdown` comes from the content selected for the note body, which is either full article content or the summary when full article content is unavailable or disabled. That means the two fields are identical only when the reader exposes no separate full content, or when the selected note content falls back to the summary. Typical reader field coverage: | Reader | `author` | `feedTitle` / `feedUrl` | `publishedAt` | `updatedAt` | `contentHtml` | `summaryHtml` | Notes | | --- | --- | --- | --- | --- | --- | --- | --- | | Google Reader-compatible | Usually | Usually | Usually | Usually | Sometimes | Sometimes | Depends on what the server exposes in `content` and `summary`. | | Inoreader | Usually | Usually | Usually | Usually | Sometimes | Sometimes | Uses the Google Reader-compatible mapping. | | Feedly | Usually | Usually | Usually | Usually | Sometimes | Sometimes | Often provides both full content and summary, but not for every entry. | | Tiny Tiny RSS | Usually | Usually | Usually | No | Usually | Sometimes | `summaryHtml` comes from TT-RSS `excerpt`. | | Fever API | Sometimes | Usually | Usually | No | Usually | No | No separate summary field is currently mapped. | | Miniflux | Sometimes | Usually | Usually | Usually | Usually | No | No separate summary field is currently mapped. | `Usually` and `Sometimes` reflect both the upstream API and the specific item. A reader may omit fields for some entries even when it supports them in general. If **Fetch article source text** is enabled, `contentHtml`, `contentMarkdown`, `contentSource`, and `contentFetchedAt` can also change after the initial reader import because the plugin may replace reader content with extracted article content. Example template: ```md --- title: <% JSON.stringify(rss.title) %> url: <% JSON.stringify(rss.url) %> reader: <% JSON.stringify(rss.reader) %> imported: <% JSON.stringify(rss.importedAt) %> rss_hash: <% JSON.stringify(rss.shortHash) %> tags: <%* for (const tag of rss.tags) { tR += ` - ${JSON.stringify(tag)}\n`; } -%> --- # <% rss.title %> <% rss.byline %> [Read original](<% rss.url %>) <% rss.contentMarkdown %> ``` If Templater is not installed, the plugin still replaces simple placeholders such as `{{rss.title}}`, `{{rss.contentMarkdown}}`, and `{{content.markdown}}`. Templater JavaScript blocks only run when Templater is installed. Use templates you trust, because Templater templates can execute JavaScript. If **Include debug view** is enabled, imported notes also end with a collapsed Obsidian callout. Use **Formatted JSON** to inspect the exact normalized values visible to templates after local enrichment, **Raw API JSON** to inspect the preserved per-item payload from the reader before local processing, or **Field view** for a one-field-per-line summary of the normalized item. The raw debug view shows the item object returned inside the reader response, not the entire paginated API response envelope. The same preserved payload is also available to templates as `rss.rawApiItem`. If you want to perform additional AI processing of the imported news items (e.g. automatic tagging, abstract writing etc.), consider using the [AI for Templater](https://community.obsidian.md/plugins/ai-templater) plugin. ## Development Install dependencies: ```bash npm install ``` Run a development build with watch mode: ```bash npm run dev ``` Create a production build: ```bash npm run build ``` ## Release Release tags must exactly match `manifest.json`'s `version` and must not use a leading `v`. When a GitHub release is published, or when the release workflow is run manually, GitHub Actions builds the plugin and uploads these required release assets: - `manifest.json` - `main.js` - `styles.css` The workflow fails if any asset is missing or empty, or if the tag does not match the manifest version. ## References - [Obsidian build a plugin guide](https://docs.obsidian.md/Plugins/Getting%20started/Build%20a%20plugin) - [FreshRSS Google Reader-compatible API](https://freshrss.github.io/FreshRSS/en/developers/06_GoogleReader_API.html) - [FreshRSS Fever API](https://freshrss.github.io/FreshRSS/en/developers/06_Fever_API.html) - [Tiny Tiny RSS API reference](https://tt-rss.org/docs/API-Reference.html) - [Inoreader OAuth 2.0 docs](https://www.inoreader.com/developers/oauth) - [Inoreader app registration](https://www.inoreader.com/developers/register-app) - [Inoreader app authentication](https://www.inoreader.com/developers/app-auth) - [Inoreader stream IDs](https://www.inoreader.com/developers/stream-ids) - [Feedly collect articles API](https://developers.feedly.com/reference/collect-articles) - [Miniflux API reference](https://miniflux.app/docs/api.html) - [Defuddle](https://github.com/kepano/defuddle)