A tool to generate an RSS feed combining your starred/favorite content from Mastodon favorites and/or bookmarks, LinkedIn reactions and any personalized RSS feed you may have (for example Feedbin or Linkding or even GitHub). A GitHub Action rebuilds and publishes the feed every hour.
- Combines multiple star sources into a single RSS feed
- Configurable number of items per feed
- Support for Mastodon media attachments in feed items
- Hashtags of Mastodon posts become feed categories (tags)
- Environment variable support for sensitive tokens
- Detailed logging options
- Short headlines for Mastodon and LinkedIn posts, generated via OpenRouter and cached
- LinkedIn reactions (except plain likes) as a source, see below
- Clone this repository
- Install dependencies with
uv sync - Copy
.env.sampleto.envand configure your tokens (includingOPENROUTER_API_KEYfor headlines) - Create a
sc_config.yaml(see below) and configure your settings
Create a sc_config.yaml file with your settings:
mastodon:
access_token: "your-token-here" # Can also be set via MASTODON_ACCESS_TOKEN env var
mastodon_instance: "https://your.instance"
mastodon_username: "yourusername"
import_tags: true # use the hashtags of a post as feed categories (default)
# Optional RSS configuration
rss:
urls:
- url: https://feedbin.com/starred/${FEEDBIN_ID}.xml
tag: feedbin
- url: https://bookmarks.ping13.net/feeds/${LINKDING_ID}/all
tag: linkding
# Optional: headline generation, see "Headline generation"
titles:
model: anthropic/claude-haiku-5.5
# Optional: LinkedIn reactions, see "LinkedIn reactions"
linkedin:
enabled: true
exclude_reactions: [LIKE]# Basic usage (outputs to stdout)
uv run rss.py
# With options
uv run rss.py --config sc_config.yaml --output feed.xml --limit 10 --debug
Options:
-c, --config TEXT Path to configuration file (default: sc_config.yaml)
--debug / --no-debug Enable debug output
-o, --output TEXT Output file (optional, defaults to stdout)
-l, --limit INTEGER Number of feed items to include (default: 5)
-L, --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
Set logging level (default: ERROR)
--help Show this message and exit- Spotify
- GitHub stars
- ...
- Currently, there is no way to use the creation date of a like in Mastodon, see this issue.
Reactions on LinkedIn posts (except plain likes) and your reposts can be added as a star source through LinkedIn's Member Data Portability API (EEA/Switzerland members only):
- Create a LinkedIn developer app with the Member Data Portability API
(Member) product and add
http://localhost:8799/callbackas redirect URL. - Put
LINKEDIN_CLIENT_IDandLINKEDIN_CLIENT_SECRETinto.env, then runmake linkedin-token(once a year). Usemake linkedin-token-cito also update the GitHub secretLINKEDIN_ACCESS_TOKENand the variableLINKEDIN_TOKEN_EXPIRES. - Enable the
linkedin:block insc_config.yaml.exclude_reactionstakes reaction types (LIKE,PRAISE,EMPATHY, ...) andREPOST.
The API only returns the last 28 days, so reactions are accumulated in
linkedin-state.json, which is published next to the feed and re-read on the
next run. The state also remembers the last processed event, so a normal run
asks LinkedIn only for new events (one request, which keeps well below the
daily API quota); once a week, or when the state is missing, the whole
28-day window is read again. Post text is read from the public post page and turned into a
headline. If the token expires, the feed still builds from the stored items and
a warning is logged.
Token renewal. The token is valid for about a year. When fewer than 30 days
remain, the feed job opens a GitHub issue ("LinkedIn token expires soon")
and you get the usual GitHub notification. Renew with
make linkedin-token-ci and close the issue. The expiry date is kept in the
repository variable LINKEDIN_TOKEN_EXPIRES.
Mastodon posts have no title, so one is generated with an LLM through
OpenRouter (needs OPENROUTER_API_KEY; without it, the
start of the post is used). Configure it in sc_config.yaml:
titles:
model: anthropic/claude-haiku-5.5 # default
fallback_model: qwen/qwen3.7-flash # used if the primary fails
max_new_titles_per_run: 100 # cost guardTitles are cached in title-generator.cache (keyed by model, prompt and text),
so only new posts cost money: typically well under USD 0.10 per month. The
GitHub Action persists the cache between runs. Compare models with
make title-bakeoff.
The feed is rebuilt every hour. When its items changed, the job starts the
favorites.yaml workflow in ping13/blog,
which rebuilds and uploads only the blog's favorites page. This needs the secret
BLOG_DISPATCH_TOKEN: a fine-grained token for ping13/blog with Actions:
Read and write.
Token renewal. When fewer than 30 days remain until the date in the
repository variable BLOG_DISPATCH_TOKEN_EXPIRES, the feed job opens a
GitHub issue ("Blog dispatch token expires soon") with the renewal steps.
linkding fills a bookmark's description from the page's meta description when
it is saved. Some sites answer with a consent page, a login wall or an app
shell instead (for example m.youtube.com links get "Auf YouTube findest du
..."), so the feed shows no description or a generic one.
bookmark_summaries.py replaces those descriptions with a 2–3 sentence
summary: Kagi's Extract API fetches the page as
Markdown (on Kagi's servers, so CI is not served bot-check pages) and the
OpenRouter model of the titles: block writes the summary. For a Hacker News
story the linked article is summarized. Error pages, walls and junk text are
recognized by the model and skipped.
A description counts as generic if it is empty, the text of an error page
("404 Fehler"), shared by bookmarks with different URLs, or the same as the
site's homepage description (unless the bookmark is that homepage). Anything
else, short notes included, is treated as written by hand and left alone.
Bookmarks with a tag from rss.exclude_categories are never sent anywhere.
- Put
LINKDING_API_TOKEN(linkding: Settings → Integrations) andKAGI_API_TOKEN(a v1 key from the Kagi API dashboard, with prepaid credit) into.env, next toOPENROUTER_API_KEY. make bookmarkslists the candidates of the last 7 days and why they were picked; nothing paid is called.make bookmarks ARGS="--days 300"looks further back.make bookmarks-previewadds the summaries they would get, without writing them.make bookmarks-applywrites them.
The feed workflow upload.yaml runs every hour and starts with --apply,
with the three keys as GitHub secrets; the feed is built afterwards, also when
summarizing failed. Hourly runs only look at bookmarks of the last 3 hours, so
a page that cannot be summarized is not retried every hour; the run at 02:04
UTC covers the last week. bookmarks.yaml runs the same step on its own, for
manual runs. Settings are in the
bookmarks: block of sc_config.yaml. A summary costs about one cent ($0.004
for Kagi Extract plus the model); max_summaries_per_run caps a run.
make help # all targets, grouped by purpose
make test # run tests (no network)
make lint # ruffArchitecture diagrams live in docs/diagrams/.