Tiny RSS

tt-rss.org
Tiny RSS

A web-based news feed reader and aggregator, supporting RSS/Atom feeds. It's free, open source, and offers a customizable and self-hostable platform for managing your news feeds.

Open Source

Tiny RSS Source Code

Author

tt-rss

Description

A free, flexible, open-source, web-based news feed (RSS/Atom/other) reader and aggregator.

#rss#rss-aggregator#rss-generator#tt-rss#ttrss

Homepage

License

GPL-3.0

Created

03 Oct 25

Last Updated

18 Jun 26

Primary Language

PHP

Size

136,826 KB

Stars

762

Forks

78

Watchers

762

Language Usage

Language Usage

Star History

Star History

Recent Commits

  • supahgreg (18 Jun 26)

    Bump the 'web-nginx' base image to nginx 1.31.2.

  • Weblate (bot) (17 Jun 26)

    Translated using Weblate (Russian) (#372) Currently translated at 100.0% (727 of 727 strings) Translation: TinyTinyRSS/webui Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/ru/ Co-authored-by: Xapitonov <[email protected]>

  • Greg (17 Jun 26)

    Bump 'guzzlehttp/guzzle' to 7.12.0. (#371)

  • Andrew Gaul (15 Jun 26)

    Fix native long-press leaking a tap and stranding the menu (#360) (#369) On Android the browser fires the contextmenu for a long-press itself, so the menu opens via the native path; the synthetic path #360 added is only reached on iOS. But armReleaseSwallow() -- which eats the finger-lift's compatibility mouse events -- was wired only into the synthetic path. So on Android (e.g. Firefox) the release fell through to the row: the feed tree's onClick ran Feeds.open() and collapsed the drawer, i.e. a long-press behaved like a tap. And once the menu was open, pending was null, so pointermove ignored a drag and the popup stayed open instead of dismissing. - The native contextmenu handler now arms armReleaseSwallow() when a touch press is in flight (pending is only ever set for touch, so a real mouse right-click is left alone), matching the synthetic path. - Track the origin of the press that opened a menu (opened) and, if the still-down finger drags past the slop, close the popup -- a drag means the user means to scroll, not pick an item. Reset per gesture on pointerdown / pointerup / pointercancel. References #360. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

  • Weblate (bot) (15 Jun 26)

    Translated using Weblate (Indonesian) (#368) Currently translated at 100.0% (727 of 727 strings) Translation: TinyTinyRSS/webui Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/id/ Co-authored-by: Dony Wibowo <[email protected]>

  • dependabot[bot] (15 Jun 26)

    npm: bump the development-dependencies group across 1 directory with 4 updates (#366) Bumps the development-dependencies group with 4 updates in the / directory: [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js), [@stylistic/eslint-plugin](https://github.com/eslint-stylistic/eslint-stylistic/tree/HEAD/packages/eslint-plugin), [eslint](https://github.com/eslint/eslint) and [less](https://github.com/less/less.js). Updates `@eslint/js` from 9.39.4 to 10.0.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js) Updates `@stylistic/eslint-plugin` from 5.6.1 to 5.10.0 - [Release notes](https://github.com/eslint-stylistic/eslint-stylistic/releases) - [Changelog](https://github.com/eslint-stylistic/eslint-stylistic/blob/v5.10.0/CHANGELOG.md) - [Commits](https://github.com/eslint-stylistic/eslint-stylistic/commits/v5.10.0/packages/eslint-plugin) Updates `eslint` from 10.4.1 to 10.5.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.4.1...v10.5.0) Updates `less` from 4.6.4 to 4.6.6 - [Release notes](https://github.com/less/less.js/releases) - [Changelog](https://github.com/less/less.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/less/less.js/commits/v4.6.6) --- updated-dependencies: - dependency-name: "@eslint/js" dependency-version: 10.0.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: development-dependencies - dependency-name: "@stylistic/eslint-plugin" dependency-version: 5.10.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies - dependency-name: eslint dependency-version: 10.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies - dependency-name: less dependency-version: 4.6.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • supahgreg (15 Jun 26)

    Address some unnecessary JS assignments. These get flagged as 'no-useless-assignment' in ESLint 10.x (https://github.com/tt-rss/tt-rss/pull/366).

  • supahgreg (14 Jun 26)

    Check for JS dev dependency updates less often + group into a single PR.

  • supahgreg (13 Jun 26)

    Bump to Alpine 3.24.1. https://alpinelinux.org/posts/Alpine-3.24.1-released.html

  • Andrew Gaul (13 Jun 26)

    Set mobile keyboard and password-manager hints on the login form (#361) Phone keyboards capitalise and autocorrect the login field by default, and password managers need the autocomplete roles to offer credentials reliably on mobile. - Login: autocomplete="username", autocapitalize="none", autocorrect="off", spellcheck="false". - Password: autocomplete="current-password". The dojo parser forwards autocorrect to the widget as a param just like the other attributes; whether it reaches the rendered input is then gated on the browser exposing a matching IDL property. WebKit does, so iOS Safari -- the only engine with an autocorrect feature -- renders it. Chromium has neither the property nor the feature, so the attribute is simply absent there, with nothing lost. Verified on the rendered login page that the attributes survive the parser and that logging in still works, and that simulating WebKit's autocorrect IDL property makes the attribute reach the input. References #157. Co-authored-by: Claude Fable 5 <[email protected]>

  • Andrew Gaul (12 Jun 26)

    Open context menus with a long-press on iOS (#360) iOS Safari never fires a contextmenu event for a touch long-press, and contextmenu is the only trigger the delegated dijit.Menu bindings listen for, so the headline and feed-tree context menus were unreachable on iPhones and iPads. Android fires the event natively at ~500ms and already worked. - App.initLongPressContextMenu() times the press itself via pointer events on #headlines-frame and #feeds-holder and dispatches the contextmenu event dijit expects at the pressed element; selector delegation, currentTarget and menu position behave exactly as for a real right-click. - The 600ms timer sits above Android's native delay on purpose: where a native contextmenu exists it wins and cancels the pending press, and a late native duplicate arriving just after a synthesised open is eaten before dijit can open the menu twice. Movement past 10px, pointerup/pointercancel or a second finger cancels the press, so scrolling, swipe-to-read and pinch are unaffected. - The menu opens while the finger is still down, so the release would land its compatibility mousedown/mouseup/click on the just-opened menu or the modal popup backdrop. Those events are swallowed at the document, armed only until just after the opening pointer lifts; the next deliberate tap (e.g. on a menu item) goes through normally. - (pointer: coarse) CSS applies user-select / -webkit-touch-callout none to the menu-bearing elements (headline title wraps, the vfeed chip, group headers, feed tree rows) so iOS's link-preview sheet and selection callout don't fire on the same press; article content stays selectable. Verified end to end on the dev instance with synthetic pointer events (untrusted events bypass Chromium's native long-press gesture, reproducing iOS's missing contextmenu): long-press opens the correct row's menu on the phone and tablet layouts, menu items execute (starred state and DB flip and stick), the release tap is swallowed, quick taps and scrolls pass through untouched, Android's native path stays single-fire, the device Back gesture still closes the menu first, and desktop right-click is unchanged. References #157. Co-authored-by: Claude Fable 5 <[email protected]>

  • Weblate (bot) (12 Jun 26)

    Translated using Weblate (Albanian) (#358) Currently translated at 96.9% (705 of 727 strings) Translation: TinyTinyRSS/webui Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/sq/ Co-authored-by: Besnik Bleta <[email protected]>

  • Greg (12 Jun 26)

    Bump to Alpine 3.24. (#357)

  • Andrew Gaul (12 Jun 26)

    Clamp article titles at two lines on the narrow layout (#359) Closed rows on phones previously truncated the title to a single nowrap line. Let the title block (title plus inline preview or excerpt) wrap onto a second line instead, clamped with an ellipsis via -webkit-line-clamp, in both list (.hl) and combined (.cdm.expandable) rows. Open (.active) combined rows keep their existing full wrap, and the docked desktop layout is unchanged. The clamped element trades its bottom padding for a margin because the CDM excerpt is a sibling of the clamp-truncated title anchor: it still lays out lines past the clamp, and their glyph tops would otherwise paint through the padding before the overflow clip. The excerpt's own nowrap is relaxed so it can flow through the second line. References #157. Co-authored-by: Claude Fable 5 <[email protected]>

  • Andrew Gaul (12 Jun 26)

    Add a Progressive Web App manifest and icons (#331) Tiny Tiny RSS can now be installed to a phone or desktop home screen and launched standalone, without browser chrome. - manifest.webmanifest: name/short_name, standalone display, theme and background colors, and 192/512/maskable icons. All paths are relative, so it is agnostic to the app's base path / subdirectory. - Icons generated from images/favicon-512px.png; the maskable variant pads the logo into the safe zone on an opaque background for Android's masks. - index.php: manifest link, light/dark theme-color, apple-touch-icon and the Apple web-app meta tags. No service worker on purpose: current browsers no longer require one for install / Add to Home Screen, and the app's static assets are already cache-busted by mtime, so it would only duplicate ordinary HTTP caching. Note: the Android install prompt requires a secure context (HTTPS, or localhost). iOS "Add to Home Screen" works over plain HTTP via the manifest and Apple meta tags alone. References #157. Co-authored-by: Claude Fable 5 <[email protected]>

  • Andrew Gaul (09 Jun 26)

    Make Dijit popups modal on narrow screens (#356) On the narrow/phone layout the feed drawer is modal -- a full-screen backdrop catches the outside tap that dismisses it -- but Dijit popups (the toolbar Actions menu, long-press headline context menus, and every other dropdown) were not: an outside tap closed the menu but the same tap also fell through and activated whatever was behind it. Give the popups the same treatment. Because every menu/dropdown opens through the single dijit.popup singleton, hooking its open()/close() (already hooked for the Back gesture) lets one #popup-backdrop cover them all: App._syncPopupBackdrop() shows a full-screen layer at z-index 999 -- just beneath the popup wrappers, which dijit.popup numbers from 1000 -- whenever a popup is open, and hides it once the last one closes. Syncing the backdrop is a pure DOM toggle, so unlike the history work it is safe to drive from a close() hook. The backdrop is transparent: it changes behaviour, not appearance, so the menus look exactly as before. It dismisses on the press (touchstart) and preventDefault()s it, which suppresses the synthesised "compatibility" click a tap produces -- otherwise closing the menu removes the backdrop mid-gesture and that click lands on the element now exposed beneath (a click-through, notably on iOS Safari). Reacting to the press rather than the finger-up also keeps the gesture that opened the menu from dismissing it the instant the finger lifts; a click handler covers non-touch input. Gated on the narrow layout (isNarrowLayout(), and the shown rule lives in the narrow media query), so the docked desktop layout is unaffected and its popups stay non-modal. Themes recompiled via gulp less. References #157. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

  • Andrew Gaul (09 Jun 26)

    Use delegated context menus in the feed tree (#342) _createTreeNode built a dijit.Menu plus 1-4 MenuItems for every feed and category node and bound it to that node. Each widget is retained in dijit.registry for the whole session, so the feed tree's menu cost grew with the number of feeds — a regular feed alone created 6 widgets (Menu + 4 items + separator), and the total scales linearly with the tree. Replace this with four delegated menus created once and bound to the whole tree via dijit.Menu's `selector`, exactly like Headlines.initHeadlinesMenu(). Each node now only gets a marker class (feedMenuFeed / feedMenuSpecial / feedMenuCat / feedMenuCatAll); the menu reads the row's data-feed-id when it opens. A destroy() override frees the four menus when the tree is rebuilt (Feeds.destroyRecursive on feed edit/subscribe) so they don't accumulate. Menu items and actions are unchanged. The per-tree menu-widget count is now a constant 13 instead of ~2.3 per node: ~620 fewer widgets (~1-2 MB plus a few thousand DOM nodes) for a 100-feed tree, scaling further with feed count. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

  • Andrew Gaul (09 Jun 26)

    Release headline rows on feed switch to stop a DOM/widget leak (#340) Opening a feed wipes the list with headlines-frame.innerHTML = ... (and renderAgain() swaps rows with replaceChild). Both detach the old rows but never destroy the per-row dijit.form.CheckBox widget, which stays pinned in dijit/registry, nor disconnect the per-row observers (the row_observer MutationObserver and the sticky/unpack IntersectionObservers). Those strong references keep every removed row and all its listeners alive until a full page reload, so DOM nodes, listeners, and widgets grow without bound as you navigate between feeds. Add a teardownRows() helper that destroys the widgets inside #headlines-frame (dijit.registry.findWidgets(...).destroyRecursive()) and disconnects the four per-row observers, and call it before the innerHTML replace in onLoaded() and before the replaceChild loop in renderAgain(). render()/onLoaded re-create the widgets and re-observe the new rows immediately afterwards, so the list is unaffected; only the outgoing rows are released. This mirrors the existing App.cleanupMemory() pattern used for the article content pane. Measured with headless Chrome over 40 alternating feed opens (post-GC): DOM nodes drop from 91,483 to 5,706, listeners from 16,136 to 925, and the page renderer's resident memory from ~217 MB to ~191 MB (~25 MB reclaimed, ~0.6 MB per feed switch on small feeds, more on larger ones). Counts now stay flat across feed switches and view re-renders instead of climbing. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

  • Andrew Gaul (09 Jun 26)

    Close an open menu or popup with the device Back gesture (#339) Extends the Back-gesture overlay handling so that on narrow/phone layouts the device Back gesture (and browser Back) first dismisses an open Dijit menu/popup -- the toolbar "Actions" hamburger menu, a long-press headline context menu, the feed-tree context menus, a toolbar dropdown -- before it closes the feed drawer or an open article, instead of leaving the app. The overlay stack becomes [article, drawer, popup], a popup always being topmost. Every popup opens through the single dijit.popup manager, so an open hook reconciles the overlay history after each open() to push a "popup" history entry, and reconcileOverlayHistory() treats a non-empty dijit.popup._stack as that topmost entry. Implementation note -- why only open() is hooked, not close(): Dijit dismisses menus on its own (an outside tap, a blur, the viewport change a phone back-swipe triggers). Removing the popup's history entry at close time requires a synthetic history.back(), which races the user's real Back: the back-swipe dismisses the menu first, the synthetic back then eats the "popup" entry, and the real Back finds nothing to absorb it and leaves the app -- exactly the reported failure. So close() is left unhooked and the entry lingers harmlessly: a Back pops it (the popstate handler closes the popup, or no-ops if Dijit already dismissed it, absorbing the press), and reconcileOverlayHistory() prunes any leftover entry on the next overlay change. The drawer was unaffected because it never self-dismisses this way. References #157. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

  • Andrew Gaul (09 Jun 26)

    Fix five client-side JS logic errors (#354) - Feeds.js: open() compared a string feed id from the sidebar tree against the numeric active id with ===, so re-clicking the active feed no longer forced an update (regression from the eqeqeq migration, commit 39182a76e); compare as strings. - Headlines.js: onTagsUpdated() used an unterminated attribute selector (missing ]) that threw SyntaxError on every tag edit; add the bracket. - Headlines.js: the vgroup feed-title context menu targeted a nonexistent class (div.cdmFeedTitle); point it at the rendered div.feed-title. - PrefHelpers.js: the stylesheet Apply handler assigned the dialog form object to <style>.innerText (rendering [object Object]); assign .value. - PrefHelpers.js: a failed plugin install guarded its stdout block on a misspelled reply.stdour; correct to reply.stdout. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

  • Andrew Gaul (09 Jun 26)

    Enable additional high-value ESLint rules (#353) Add 16 rules that currently have zero violations across js/ and plugins/: the eval-family security rules (no-eval, no-implied-eval, no-new-func, no-script-url, no-extend-native), correctness/bug-catching rules (array-callback-return, no-return-assign, no-self-compare, no-unmodified-loop-condition, no-unreachable-loop, no-constructor-return, no-new-wrappers), and modern-syntax rules complementing the existing prefer-const (no-var, prefer-spread, prefer-object-spread, no-useless-rename). These act as a regression ratchet only; no source changes were required and 'npm run lint:js' stays clean. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>

  • Greg (08 Jun 26)

    Merge pull request #341 from tt-rss/weblate-integration Translations update from Hosted Weblate

  • Anarion Dunedain (08 Jun 26)

    Translated using Weblate (Polish) Currently translated at 100.0% (727 of 727 strings) Translation: TinyTinyRSS/webui Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/pl/

  • Hosted Weblate user 54392 (07 Jun 26)

    Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (727 of 727 strings) Translation: TinyTinyRSS/webui Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hans/

  • TonyRL (07 Jun 26)

    Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (727 of 727 strings) Translation: TinyTinyRSS/webui Translate-URL: https://hosted.weblate.org/projects/tt-rss/webui/zh_Hant/

  • supahgreg (08 Jun 26)

    Bump 'guzzlehttp/guzzle' to 7.11.1.

  • supahgreg (08 Jun 26)

    Bump 'spomky-labs/otphp' to 11.5.0.

  • dependabot[bot] (08 Jun 26)

    npm: bump eslint from 9.39.1 to 10.4.1 (#348) Bumps [eslint](https://github.com/eslint/eslint) from 9.39.1 to 10.4.1. - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v10.4.1) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.4.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • supahgreg (08 Jun 26)

    Add dev dependency '@eslint/js'.

  • dependabot[bot] (08 Jun 26)

    npm: bump stylelint from 16.26.1 to 17.13.0 (#351) Bumps [stylelint](https://github.com/stylelint/stylelint) from 16.26.1 to 17.13.0. - [Release notes](https://github.com/stylelint/stylelint/releases) - [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md) - [Commits](https://github.com/stylelint/stylelint/compare/16.26.1...17.13.0) --- updated-dependencies: - dependency-name: stylelint dependency-version: 17.13.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Tiny RSS Website

Website

Home | Tiny Tiny RSS

Tiny Tiny RSS (tt-rss) is a free, flexible, open-source, web-based news feed (RSS/Atom/other) reader and aggregator.

Redirects

Does not redirect

Security Checks

All 65 security checks passed

Server Details

  • IP Address 185.199.110.153
  • Hostname cdn-185-199-110-153.github.com
  • Location California, Pennsylvania, United States of America, NA
  • ISP GitHub Inc.
  • ASN AS54113

Associated Countries

  • US US

Safety Score

Website marked as safe

100%

Blacklist Check

tt-rss.org was found on 0 blacklists

  • AntiSocial Blacklist
  • Artists Against 419
  • Badbitcoin
  • Bambenek Consulting
  • CERT Polska
  • CoinBlockerLists
  • CRDF
  • CryptoScamDB
  • EtherAddressLookup
  • EtherScamDB
  • Fake Website Buster
  • MetaMask EthPhishing
  • NABP Not Recommended Sites
  • OpenPhish
  • PetScams
  • PhishFeed
  • PhishFort
  • Phishing.Database
  • PhishStats
  • PhishTank
  • Phishunt
  • RPiList Not Serious
  • Scam.Directory
  • SecureReload Phishing List
  • Spam404
  • StopGunScams
  • Suspicious Hosting IP
  • ThreatFox
  • ThreatLog
  • TweetFeed
  • URLhaus
  • ViriBack C2 Tracker

Website Preview

Website preview

Tiny RSS Docker

Container Info

tt-rss

Tiny Tiny RSS is an open source web-based news feed (RSS/Atom) reader and aggregator, designed to allow you to read news from any location, while feeling as close to a real desktop application as possible.

#Other lunik1/tt-rss:latest

Run Command

docker run -d \
  -p 80/tcp \
  -e PUID=${PUID} \
  -e PGID=${PGID} \
  -v /portainer/Files/AppData/Config/tt-rss:/config \
  --restart=unless-stopped \
  lunik1/tt-rss:latest

Compose File

version: 3.8
services:
  tiny-tiny-rss:
    image: "lunik1/tt-rss:latest"
    ports:
      - 80/tcp
    environment:
      PUID: 1000
      PGID: 100
    volumes:
      - "/portainer/Files/AppData/Config/tt-rss:/config"
    restart: unless-stopped

Environment Variables

  • Var Name Default
  • PUID 1000
  • PGID 100

Port List

  • 80/tcp

Volume Mounting

  • /portainer/Files/AppData/Config/tt-rss /config

Tiny RSS Reviews

More News Readers

⚠️ This section is still a work in progress ⚠️
Check back soon, or help us complete it by submiting a pull request on GitHub.
Or submit an entry here

About the Data: Tiny RSS

Change History

API

You can access Tiny RSS's data programmatically via our API. Simply make a GET request to:

https://api.awesome-privacy.xyz/v1/services/tiny-rss

The REST API is free, no-auth and CORS-enabled. To learn more, view the API Docs or read the API Usage Guide.

Share Tiny RSS

Help your friends compare News Readers, and pick privacy-respecting software and services.
Share Tiny RSS and Awesome Privacy with your network!

View News Readers (1)