{"id":50973452,"url":"https://github.com/boringcode-dev/feedreader","last_synced_at":"2026-06-19T05:01:27.924Z","repository":{"id":365642102,"uuid":"1273062390","full_name":"boringcode-dev/feedreader","owner":"boringcode-dev","description":"A tiny, fast, self-hosted feed reader for engineering and research signals.","archived":false,"fork":false,"pushed_at":"2026-06-18T09:08:00.000Z","size":280,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-18T09:14:13.736Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/boringcode-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-18T07:13:08.000Z","updated_at":"2026-06-18T09:08:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/boringcode-dev/feedreader","commit_stats":null,"previous_names":["boringcode-dev/feedreader"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/boringcode-dev/feedreader","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringcode-dev%2Ffeedreader","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringcode-dev%2Ffeedreader/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringcode-dev%2Ffeedreader/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringcode-dev%2Ffeedreader/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/boringcode-dev","download_url":"https://codeload.github.com/boringcode-dev/feedreader/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/boringcode-dev%2Ffeedreader/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34517750,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-19T05:01:26.802Z","updated_at":"2026-06-19T05:01:27.914Z","avatar_url":"https://github.com/boringcode-dev.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# feedreader\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eA tiny, fast, self-hosted feed reader for engineering and research signals.\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  Server-rendered UI · SQLite storage · Scheduled refresh · Docker-friendly · Private by default\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Go-1.24-00ADD8?logo=go\u0026logoColor=white\" alt=\"Go 1.24\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/SQLite-3-003B57?logo=sqlite\u0026logoColor=white\" alt=\"SQLite\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Docker-ready-2496ED?logo=docker\u0026logoColor=white\" alt=\"Docker ready\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Frontend-server--rendered-111827\" alt=\"Server rendered UI\" /\u003e\n\u003c/p\u003e\n\n---\n\n## Screenshot\n\n![feedreader home screen](docs/assets/feedreader-home.png)\n\n---\n\n## Features\n\n- **Multi-source feed aggregation**\n  - Hacker News\n  - GitHub Trending\n  - Hugging Face Papers Trending\n  - alphaXiv Explore\n- **Persistent local storage** with SQLite\n- **Incremental fetch model** that keeps older items in the database\n- **Server-backed incremental loading**: first page loads 12 items, first-load bootstrap/filter/search/refresh show a toast-based loading state, and `View more` appends more items in place\n- **Source-aware card summaries**\n  - Hacker News cards show **points** and **comments**\n  - GitHub cards show **stars**, **today's stars**, and **forks**\n  - Hugging Face cards show **upvotes**\n  - alphaXiv cards show **likes**\n- **Responsive, minimalist UI** with:\n  - source filters\n  - real source icons in filters, dialog rows, and card metadata\n  - RSS-based app icon/favicon branding\n  - dark/light mode\n  - inline expanding search\n  - refresh control\n  - source configuration dialog\n- **Configurable visible sources** stored in `localStorage`\n  - choose which source buttons are shown\n  - when 2+ sources are enabled, `All` stays visible and aggregates over the enabled set\n  - when exactly 1 source is enabled, only that source button is shown\n- **Debounced client-side search UX** backed by the server API\n- **Explicit empty states** for no-result source filters and searches\n- **Connectivity toasts** for internet disconnect/reconnect events\n- **Scheduled refresh** every 3 hours on wall-clock boundaries in UTC+7\n- **Manual refresh** from the UI updates the current feed list in place without a full page reload and shows a toast-based loading state while refresh + refetch are running\n- **Persisted visited-link dimming** for feed card titles across reload/reopen using local storage\n- **PWA-ready assets and offline caching** including manifest, service worker, touch icons, cached shell assets, and cached `/api/items` responses for previously visited views\n- **Reconnect list refresh** re-fetches the current view from backend stored items only; it does **not** refresh upstream sources\n- **Docker deployment** with reverse-proxy-friendly HTTP service\n\n---\n\n## Why feedreader?\n\n`feedreader` is designed for people who want a small, understandable, self-hosted reader instead of a large feed platform.\n\nIt optimizes for:\n\n- simple operations\n- low memory usage\n- straightforward data ownership\n- easy extension when adding more sources\n\n---\n\n## Tech stack\n\n### Backend\n\n- Go\n- `net/http`\n- `html/template`\n- `modernc.org/sqlite`\n- `goquery`\n\n### Frontend\n\n- Server-rendered HTML\n- Vanilla JavaScript\n- Plain CSS\n\n### Storage\n\n- SQLite\n\n### Deployment\n\n- Docker\n- Reverse proxy compatible\n\n---\n\n## Architecture\n\nAt a high level:\n\n1. source adapters fetch upstream content\n2. items are upserted into SQLite by `(source, external_id)`\n3. the web app reads stored items ordered by article date descending\n4. the scheduler refreshes on 3-hour clock boundaries\n\nKey properties:\n\n- old items are retained in the database\n- fetch failures do not wipe existing data\n- sources without a native article date fall back to the initial fetch time (`first_seen_at`) for ordering\n- later refreshes preserve the original published/fetched ordering timestamps for existing items\n\n---\n\n## Project structure\n\n```text\ncmd/feedreader/         CLI entrypoint\ninternal/config/        configuration loading\ninternal/db/            SQLite bootstrap and pragmas\ninternal/domain/        domain models\ninternal/repository/    persistence layer\ninternal/service/       refresh orchestration and scheduler\ninternal/sources/       upstream source adapters\ninternal/web/           HTTP handlers and page rendering\nweb/templates/          HTML templates\nweb/static/             CSS, JS, icons, PWA assets\ndocs/assets/            README screenshots and supporting images\n```\n\nHost-level implementation notes for this deployment live at:\n\n- `~/.hermes/implementations/2026-06-18_feedreader-service-implementation.md`\n\n---\n\n## Getting started\n\n### Prerequisites\n\n- Go 1.24+ for local builds\n- or Docker for containerized usage\n\n### Run locally\n\n```bash\ngo run ./cmd/feedreader serve --host 0.0.0.0 --port 8080\n```\n\nThen open:\n\n- `http://127.0.0.1:8080`\n\n### Manual refresh\n\n```bash\ngo run ./cmd/feedreader fetch\n```\n\n### Docker build\n\n```bash\ndocker build -t feedreader .\n```\n\n### Docker run\n\n```bash\ndocker run --rm -p 8080:8080 -v $(pwd)/data:/data feedreader\n```\n\nThen open:\n\n- `http://127.0.0.1:8080`\n\n---\n\n## Configuration\n\nEnvironment variables:\n\n| Variable                             |                Default | Description                                                    |\n| ------------------------------------ | ---------------------: | -------------------------------------------------------------- |\n| `FEEDREADER_DB_PATH`                 | `./data/feedreader.db` | SQLite database path                                           |\n| `FEEDREADER_REFRESH_INTERVAL_HOURS`  |                    `3` | Refresh interval setting used by the scheduler                 |\n| `FEEDREADER_ITEMS_PER_SOURCE`        |                   `20` | Per-source item count used in source dashboard/health contexts |\n| `FEEDREADER_REQUEST_TIMEOUT_SECONDS` |                   `20` | Upstream request timeout                                       |\n| `FEEDREADER_USER_AGENT`              |       `feedreader/0.1` | Outbound fetch user agent                                      |\n| `FEEDREADER_HOST`                    |              `0.0.0.0` | HTTP bind host                                                 |\n| `FEEDREADER_PORT`                    |                 `8080` | HTTP bind port                                                 |\n\n---\n\n## Scheduling\n\nThe scheduler runs **inside the app process**.\n\nBehavior:\n\n- aligned to **UTC+7** (`Asia/Ho_Chi_Minh`)\n- runs on the next **3-hour wall-clock boundary**\n- does **not** perform an immediate refresh just because the container starts\n\nManual refresh is also available through the UI and CLI.\n\n---\n\n## API\n\n### `GET /healthz`\n\nReturns service health and per-source refresh status.\n\n### `GET /api/items`\n\nReturns feed items for incremental loading.\n\nQuery params:\n\n- `source` — optional source filter (`hackernews`, `github`, `huggingface`, `alphaxiv`)\n- `sources` — optional comma-separated aggregate source set used when the client wants the `All` view scoped to enabled sources (for example `hackernews,github`)\n- `q` — optional case-insensitive search query across title, summary, author, URL host/path, and stored metadata\n- `limit` — page size\n- `offset` — pagination offset\n\n---\n\n## Data model\n\nThe service stores a cumulative feed history.\n\nEach fetch:\n\n- upserts items by `(source, external_id)`\n- updates refresh state in `sync_state`\n- preserves older items already in the database\n\nThe UI/API render items from the full stored set, ordered by article date descending.\n\nPresentation-layer note:\n\n- the source adapters persist raw metadata into `metadata_json`\n- the card-building layer turns that metadata into user-visible summary lines\n- current rendered metrics are:\n  - Hacker News: points and comments\n  - GitHub: stars, today, forks\n  - Hugging Face Papers: upvotes\n  - alphaXiv: likes\n- source icons are not embedded in the brief text itself\n- the current card layout renders the real source icon inline before the host/domain line\n\n---\n\n## UI behavior\n\n### Search\n\n- the search control expands inline in the header\n- clicking the search icon focuses the input\n- the input renders at `16px` to avoid common iOS Safari auto-zoom behavior\n- typing is debounced before hitting the API\n- closing the search control clears the query and resets the feed\n\n### Loading and empty states\n\n- first-load bootstrap queries, source filter changes, searches, `View more`, and manual refresh all show an explicit toast-based loading state\n- source-filter and search requests that return zero items replace the list with an empty-state message instead of leaving stale cards on screen\n- `View more` disables itself while an append request is in flight and hides itself when the current result set has no further page\n\n### Offline and connectivity\n\n- the app shell and previously fetched `GET /api/items` views are cached by the service worker for offline reuse\n- this offline/PWA behavior requires a secure-context origin where service workers are available (for example `localhost` or HTTPS); plain HTTP network IP origins such as `http://100.94.224.102:9002` do not get service-worker-based offline reopen support on iOS\n- when the browser goes offline, a no-wifi indicator appears before the refresh button instead of showing connectivity toasts\n- if an offline view has no cached `/api/items` response yet, the list is replaced with `Offline and no cached items are available for this view yet.`\n- when the browser comes back online, the no-wifi indicator disappears and the current view is re-fetched silently from `/api/items`\n- reconnect refreshes backend-stored items only; the only UI path that calls `POST /api/refresh` remains the manual refresh button\n\n### Source configuration\n\n- the configure button opens a dialog that lets the user choose visible sources\n- selected sources are stored in `localStorage` under `feedreader.sources`\n- source-specific filters render as **real icon-only buttons**\n- `All` remains a text button\n- the source dialog renders **real source icons** before each source name\n- if **2 or more** sources are enabled, the filter bar shows:\n  - `All`\n  - each enabled source\n- if **exactly 1** source is enabled, the filter bar shows only that source\n- the `All` view aggregates only over the enabled source set, not over disabled sources\n\n---\n\n## Roadmap\n\nPotential next improvements:\n\n- more sources (blogs, changelogs, newsletters, papers)\n- server-side pagination\n- source weighting and ranking controls\n- source-specific parsing tests with fixtures\n- export/import support\n\n---\n\n## Contributing\n\nContributions are welcome.\n\nA good contribution flow:\n\n1. fork the repository\n2. create a branch\n3. make changes\n4. run formatting and tests\n5. open a pull request\n\nExample local verification:\n\n```bash\ngofmt -w $(find . -name \"*.go\")\ngo test ./...\n```\n\n---\n\n## Repository hygiene\n\nThe SQLite runtime data directory is intentionally ignored:\n\n```gitignore\ndata/\n```\n\nThis keeps the repository focused on source code and assets.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboringcode-dev%2Ffeedreader","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fboringcode-dev%2Ffeedreader","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fboringcode-dev%2Ffeedreader/lists"}