Model or dataset
Seanium/FeedMe avatar
Seanium/FeedMe

FeedMe builds a static RSS reader where the deploy branch is the contract, and the LLM client is a devDependency

😋 AI-powered, Lightweight RSS Reader. Supports: GitHub Pages | Vercel | Alibaba Cloud ESA Pages | Docker

748 stars167 forksTypeScriptMIT

At a glance

What is it?
Seanium's FeedMe turns a YAML feed list plus an LLM key into a static site, and ships four deployment paths that all hang off one GitHub Actions workflow pushing to a deploy branch. The interesting details are the two independent cron schedules, the split between baked image and mounted data volume, and where the summary client is declared.
Who is it for?
FeedMe fits a reader who wants a self hosted feed page with summaries, no account and no app, and who is willing to let a scheduled job spend LLM calls every hour. Before forking it, check four things.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 12, 2026, and from our analysis. They are not legal advice.

Editorial analysis

main builds, deploy serves, and the workflow runs hourly

The deployment story is one workflow with several front ends. `update-deploy.yml` runs on an hourly schedule, on push, and on manual dispatch. Each run does a single build pass: fetch the RSS sources, generate summaries, build the static site. It then publishes to GitHub Pages and pushes the build output to a `deploy` branch, which the other platforms watch.

That second branch is the contract. The repository's default branch is `main`, and nothing built lands there, so any host configured for `main` has nothing to serve.

The fetch and summarise step writes into `public/data`, and the site itself is a static bundle. Nothing in this design needs a server: the runtime dependency is a static host, and the only moving part is the workflow.

The primary README here is Chinese, with `README.en.md` as the English version, and the deployment steps come out of the Chinese one. That matters for anyone checking a detail against an English page: the English file is the shorter of the two and does not carry the same depth on configuration.

Vercel and ESA Pages both want empty build commands

The two platform paths are configured to consume an artefact rather than build one, and the field values make that explicit.

Vercel's instructions are unusually honest about the first attempt. Import the forked repository from GitHub, click Deploy, and the failure at that point is expected, because the default production branch is `main`. The fix is to change the production branch to `deploy`, set the project to build only on the production branch, and redeploy. After that, each workflow run pushes to `deploy` and Vercel detects it by itself.

ESA Pages takes the same shape with different fields: production branch `deploy`, static assets directory set to a single English period, install command left empty, build command left empty.

Empty build commands are only correct because the artefact already exists. That is the whole reason the workflow pushes to a second branch rather than letting each host run its own build, and it is also why the three hosts cannot disagree: they all serve the same bytes from the same run.

GitHub Pages is the exception, needing no second branch, since the workflow deploys to it directly.

Four secrets, and adding a summary language is a code change

Everything the summary step needs arrives as four GitHub Actions secrets, added under Settings, Secrets and variables, Actions. `LLM_API_KEY` is the key, `LLM_API_BASE` is the API base URL, `LLM_NAME` is the model name, and `SUMMARY_LOCALES` is the list of languages to generate summaries in, defaulting to `zh,en`.

The same four names are the environment variables for a local run, and `.env.example` ships working placeholder values: a base URL of `https://api.siliconflow.cn/v1` and a model of `THUDM/GLM-4-9B-0414` as the example pair.

The part that surprises people is the language list. Adding a language is not a configuration edit. You have to add locale metadata in `src/config/i18n-config.ts` and then add the interface strings for that language in the relevant localisation config, because the same variable decides which summaries get generated and which translation set the interface needs. So a new locale is a pull request, not a value in a secrets panel.

One detail does make failure survivable: the summary configuration includes fallback copy for when generation fails, so a bad response from the model service does not have to stop the build.

The feed list is a YAML file with a four field shape per source

Feeds are configured in `src/config/feedme.config.yaml`, and each source carries four fields: `id` as a stable unique identifier, `name` as a multilingual name, `url` as the feed address, and `category` as the grouping it belongs to.

Display order is not taken from the sources themselves. It comes from the order of the `categories` list, so reordering the category list reorders the whole front page, and a source whose category is not in that list has no defined place. Two settings sit next to the list: `settings.defaultSource` picks what opens first, and `settings.maxItemsPerFeed` caps how many entries a feed keeps.

The `summary` block is where the generation behaviour lives, and the keys are all adjustable: the summary prompt, the input truncation length, `temperature`, `maxTokens`, and the fallback text used when generation fails. The truncation length is the one to think about before adding long-form feeds, because it decides how much of an article reaches the model, and the other two are the ordinary trade between a cheaper and a more literal summary.

Results are not written to a database. The feed update command writes the fetched content and the generated summaries into `public/data`, which is why the build output is a static directory you can serve from anywhere.

Two cron schedules run the same job, and they are edited in different files

Update frequency is configured twice, once per deployment model, and the two are easy to confuse.

On the hosted paths the schedule lives in `.github/workflows/update-deploy.yml`, as the workflow's cron expression, and the default runs hourly. The documented example of changing it to a daily run in the early morning is:

yml
cron: '0 0 * * *'

On the Docker path there is no workflow at all, and the container runs its own cron from `src/config/crontab-docker`, also hourly by default, running `pnpm update-feeds` and `pnpm build` before restarting the service. The documented example there is `0 */6 * * *` for every six hours.

So the same job has two clocks in two files in two formats, and only one of them is present in a given deployment. Editing the workflow's cron has no effect on a Docker install, and editing `crontab-docker` has no effect on GitHub Pages.

The cost consequence follows from the same facts. Every tick fetches each feed and regenerates summaries in each configured locale, so an hourly schedule on a large feed list is a recurring LLM bill rather than a one-off build. Since `SUMMARY_LOCALES` defaults to two languages, the default configuration asks for two summary passes per article per hour.

Docker bakes the app, mounts the env read-only, and keeps data in a volume

The Docker path is four commands: clone the repository, copy the environment example, bring the compose stack up with a build, and open the site on port 3000.

bash
git clone https://github.com/Seanium/feedme.git
cd feedme
bash
cp .env.example .env
bash
docker-compose up --build

The compose file is short and the split it draws is the important part. The service builds from the repository, maps port 3000, names the container `feedme`, and restarts unless stopped. Two mounts do the work: `./.env` is mounted at `/app/.env` read-only, and a named volume `feedme-data` is mounted at `/app/public/data`.

That means credentials arrive from the host and never enter the image, while generated data survives a rebuild. Everything else is baked in at build time, so a change to the feed config or the interface needs a rebuild, not just a restart, even though the data itself persists. It also explains why `.env` is mounted rather than baked: the same image can be pointed at different keys without being rebuilt.

The image itself is `node:24-alpine`, with `dcron`, `bash` and `procps-ng` installed, pnpm 8.4.0 installed globally, and dependencies installed with a frozen lockfile after copying `package.json` and `pnpm-lock.yaml` first for layer caching. The crontab file is copied to `/etc/crontabs/root` and applied, and startup goes through a custom entrypoint script.

One small thing worth knowing: the compose file still carries a `version` key with the value quoted as a string. Modern Compose treats that key as obsolete and prints a warning about it, so the warning on first `up` is expected rather than a mistake in your setup.

The summary client is declared as a devDependency, and there is no test script

The manifest is where the two soft spots are. The `openai` package sits in `devDependencies` at `^4.96.0`, alongside `rss-parser` at `^3.13.0`, even though the fetch and summary step is the part that runs in production. `update-feeds` is `node scripts/update-feeds.ts` and `full-build` chains it with the Vite build, so the container image needs both packages to work, and the image installs them because a normal install inside Docker includes development dependencies.

The consequence is narrow but real: a production-only install would lack the client the summariser needs, and nothing in the scripts warns about it.

There is also no `test` entry in the scripts at all. The available ones are `dev`, `build`, `preview`, `typecheck`, `update-feeds` and `full-build`, and there is no test directory in the tree. For a project whose build step calls a paid external API, that means the summary and fetch path has no automated check of its own.

The rest of the manifest is conservative. The package is private and the version reads 0.1.0, the module type is ESM, the engine is constrained to Node `>=24.12.0 <25` with `.nvmrc` and `.node-version` present, and the package manager is pinned to pnpm 8.4.0, which matches the version the Dockerfile installs globally. No GitHub releases are published, so 0.1.0 is the only version string a user can read anywhere.

Editorial conclusion

FeedMe fits a reader who wants a self hosted feed page with summaries, no account and no app, and who is willing to let a scheduled job spend LLM calls every hour. Before forking it, check four things. How many feeds and how many summary locales you have, because each hourly run regenerates summaries and SUMMARY_LOCALES defaults to two languages. That you have somewhere for the build output to land, since three of the four deployment paths need the deploy branch to be the production branch. That the Docker path is the one you want if you would rather own the schedule: the container runs its own cron and keeps generated data in a volume, but a config change means rebuilding the image. And note what you are not getting: no test script, no releases, and a package version of 0.1.0, so read the workflow before trusting it with a long feed list.

Frequently asked questions

What does FeedMe need to generate AI summaries?

Four values, supplied as GitHub Actions secrets or as environment variables: LLM_API_KEY, LLM_API_BASE, LLM_NAME and SUMMARY_LOCALES. The example file pairs a base URL of https://api.siliconflow.cn/v1 with the model THUDM/GLM-4-9B-0414, and SUMMARY_LOCALES defaults to zh,en. Adding a language also requires locale metadata in src/config/i18n-config.ts.

How do I change how often the FeedMe site updates?

It depends on the deployment. On GitHub Pages, Vercel or ESA Pages the schedule is the cron expression in .github/workflows/update-deploy.yml, which runs hourly by default. On the Docker path the container reads its own schedule from src/config/crontab-docker, also hourly, running pnpm update-feeds and pnpm build. The two files are independent.

Why does my first Vercel deployment of FeedMe fail?

Because the default production branch is main, and the build output is on the deploy branch. The documented fix is to change the production branch to deploy, set Vercel to build only on the production branch, and redeploy. The GitHub Actions workflow pushes the built site to deploy after each build, after which Vercel detects it.

Where does FeedMe store the fetched feeds and generated summaries?

In the public/data directory, which the update command fills before the site is built. In the Docker setup that directory is a named volume, feedme-data, mounted at /app/public/data, so generated data survives a rebuild while the application code is baked into the image.

How do I add a new RSS source to FeedMe?

Edit src/config/feedme.config.yaml. Each source needs an id that is a stable unique identifier, a multilingual name, a url and a category, and the display order of categories comes from the order of the categories list rather than from the sources. settings.defaultSource and settings.maxItemsPerFeed control the opening source and the per feed cap.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. Seanium/FeedMe on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/seanium-feedme.svg)](https://hysenlabs.com/projects/seanium-feedme)