Skip to content
This repository was archived by the owner on Jun 22, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# AGENTS.md

## Project overview

`feedreader` is a small Go service that aggregates Hacker News, GitHub Trending,
Hugging Face Papers Trending, and alphaXiv into a private, server-rendered feed
reader backed by SQLite. It ships as a Docker container.

## Build & run

```bash
go run ./cmd/feedreader serve --host 127.0.0.1 --port 8080
```

Configuration is env-var driven — see [internal/config/config.go](internal/config/config.go)
for the full list and defaults rather than duplicating it here.

## Test

```bash
gofmt -l $(git ls-files '*.go') # must print nothing
go test ./...
```

Both checks run in CI ([.github/workflows/ci.yml](.github/workflows/ci.yml)) on every PR.

## Code style

`gofmt` is the only formatter. No additional linter is configured.

## Architecture

See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the package-by-package layout.

## Adding a new feed source

Implement the `sources.Source` interface and register it in `sources.Build()`
in [internal/sources/sources.go](internal/sources/sources.go).

## Security considerations

There is no authentication on the HTTP API surface — this is designed for
private/personal deployment behind your own network or reverse proxy. Do not
add public write endpoints beyond the existing `POST /api/refresh`.

## Commit / PR conventions

Conventional Commits (`feat:`, `fix:`, `chore:`, ...) — releases are automated
via [release-please](.github/workflows/release-please.yml) based on commit
messages.
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@AGENTS.md

## Claude-specific notes

- Don't read or modify files under `data/` (gitignored local SQLite DBs).
- Prefer `go test ./internal/<package>/...` over a full `go test ./...` run
when iterating on a single package.
40 changes: 40 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Architecture

## Packages

| Package | Responsibility |
| --- | --- |
| [`cmd/feedreader`](../cmd/feedreader) | CLI entrypoint: `serve`, `fetch`, `healthcheck` subcommands. |
| [`internal/config`](../internal/config) | Env-var configuration loading and defaults. |
| [`internal/db`](../internal/db) | SQLite bootstrap: schema DDL, WAL/pragma setup. |
| [`internal/domain`](../internal/domain) | Plain data types shared across packages (`FeedItem`, `SyncState`, `CardView`, ...). |
| [`internal/repository`](../internal/repository) | Persistence: upserts, sync-state tracking, feed queries. |
| [`internal/service`](../internal/service) | Refresh orchestration, scheduler, card-building/display logic. |
| [`internal/sources`](../internal/sources) | Upstream source adapters (Hacker News, GitHub Trending, Hugging Face Papers, alphaXiv). |
| [`internal/web`](../internal/web) | HTTP routes, SSR page rendering, JSON APIs. |

## Data model and refresh behavior

- Items are upserted by `(source, external_id)`. A refresh never deletes
existing rows — a failed fetch just records the failure in `sync_state` and
leaves prior data in place.
- The original `published_at` is preserved across re-fetches via
`coalesce(items.published_at, excluded.published_at)` — a source that later
starts reporting a different date for the same item doesn't reorder it.
- `internal/repository/sqlite.go`'s `ListFeedItems` sorts and paginates **in
application memory**, not in SQL. This is intentional: total item count
across all 4 sources is small (a few hundred rows), so the simplicity of one
in-memory comparator outweighs the complexity of expressing the same
fallback-ordering (published date, else first-seen date, else source rank)
in SQL.
- The scheduler in `internal/service/service.go` wakes on N-hour wall-clock
boundaries in `Asia/Ho_Chi_Minh` (UTC+7, no DST) — default hourly via
`FEEDREADER_REFRESH_INTERVAL_HOURS`. It does not refresh immediately on
startup.

## Frontend

Server-rendered HTML (`web/templates/index.html`) plus vanilla JS
(`web/static/app.js`) — no frontend build step. The service worker
(`web/static/service-worker.js`) caches the app shell and visited
`/api/items` responses for offline reuse.
38 changes: 38 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Deployment (Docker)

This is the only supported deployment for this repo. A Cloudflare Workers
port lives in a separate repo, `boringcode-dev/feedreader-edge`.

## Build

```bash
docker build -t feedreader .
```

CI publishes multi-arch images (`linux/amd64`, `linux/arm64`) to
`ghcr.io/boringcode-dev/feedreader` on `v*.*.*` tag pushes
([.github/workflows/cd.yml](../.github/workflows/cd.yml)).

## Run

```bash
docker run --rm -p 8080:8080 -v $(pwd)/data:/data feedreader
```

The `/data` volume holds the SQLite database (`FEEDREADER_DB_PATH`, default
`/data/feedreader.db` inside the container). Losing this volume loses all
fetched history; sources are re-fetched from scratch on the next refresh.

## Configuration

See the env var table in [README.md](../README.md#configuration) — this doc
intentionally doesn't duplicate it.

## Release flow

1. Merge to `main` — CI runs `gofmt -l` + `go test ./...`.
2. [release-please](../.github/workflows/release-please.yml) opens a release
PR based on Conventional Commit messages; merging it tags a version and
updates `CHANGELOG.md`.
3. The tag push triggers `cd.yml`, which builds and pushes the image, then
appends container-pull instructions to the GitHub release notes.
Loading