Follow source/topic — account-backed personalization (v1)

Per Codex — turn accounts into a real reason to return, without an algorithmic
feed. Durable interests (sources + tags), not moods.

* DB: user_follows (user_id, kind source|tag, value, unique).
* queries.feed gains follow_sources/follow_tags → the Following feed is
  "articles from a followed source OR carrying a followed tag", still respecting
  calm filters/boundaries.
* API: GET/POST/DELETE /api/follows (sign-in required; source ids validated);
  /api/feed?following=true resolves the user's follows (anon → empty, not error).
* Frontend: follows store (followKeys + toggleFollow, mirrors savedIds); a
  Follow button on source + tag/topic views; a "Following" lane in the nav with
  a tailored empty state; a Following management section in Account (unfollow).

Digest "From what you follow" deferred to v2 (brief stays first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-09 17:34:46 -04:00
parent 69ed202c4e
commit d8e246b4ff
7 changed files with 267 additions and 12 deletions
+9
View File
@@ -257,6 +257,15 @@ CREATE TABLE IF NOT EXISTS feedback_replies (
);
CREATE INDEX IF NOT EXISTS idx_feedback_replies_fid ON feedback_replies(feedback_id);
CREATE TABLE IF NOT EXISTS user_follows (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
kind TEXT NOT NULL, -- 'source' | 'tag'
value TEXT NOT NULL, -- source id (as text) or tag key
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, kind, value)
);
CREATE TABLE IF NOT EXISTS digest_sends (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,