Files
homefeed/README.md
T
Claude 5d210c961c Call out the FRONTEND_ORIGIN/ORIGIN match requirement more sharply in the deploy docs
Confirmed by reproduction: a mismatch between backend FRONTEND_ORIGIN and
frontend ORIGIN throws "CORS error: Incorrect 'Access-Control-Allow-Origin'
header is present on the requested resource" on every page, since
SvelteKit's server-side fetch enforces real CORS during SSR. Easy to trip on
since the two values live in separate .env files edited at different times.
2026-07-21 22:46:59 +00:00

172 lines
8.8 KiB
Markdown

# Homefeed — frontend, real backend, and mock backend
```
frontend/ SvelteKit app — the actual site (homepage, category pages, article view, admin panel)
backend/ The real backend — Node.js/TypeScript, SQLite, RSS/API ingestion, Ollama-backed synthesis
mock-backend/ Tiny Express server serving dummy articles — useful for pure frontend UI work without Ollama running
```
Both `backend/` and `mock-backend/` implement the identical `/api/feed`,
`/api/article/:id`, `/api/tags`, `/api/events`, `/api/admin/*` contract — the frontend
doesn't know or care which one it's talking to. Switch between them by changing
`VITE_BACKEND_URL` in `frontend/.env`.
## Running the real backend
```bash
cd backend
cp .env.example .env
npm install
npm run dev
```
The console prints an admin API key on every startup (a fresh one each time) — copy
it into the admin login page. See `backend/README.md` for details.
See `backend/README.md` for what's fully implemented vs. stubbed (Telegram adapter,
image-selection heuristic vs. vision model, etc.), and how it behaves when Ollama
isn't reachable.
## Running the mock backend instead (frontend-only work, no Ollama needed)
Two terminals:
```bash
# Terminal 1 — mock backend (http://localhost:4000)
cd mock-backend
npm install
npm start
# Terminal 2 — frontend (http://localhost:5173)
cd frontend
npm install
npm run dev
```
Open http://localhost:5173.
## What's implemented
- **Homepage** (`/`) — hero story, Local section, Business and Tech rails
- **Category pages** (`/category/local`, `/category/world`, etc.) — full listing per category, `local` maps to the Philadelphia geo filter
- **Article page** (`/article/:id`) — merge badge, hero image with single-source attribution, body, video slot, tag chips, thread continuation banners (both directions — "newer coverage" / "earlier coverage"), sources footer
- **Article cards** — show source count (`⇄ N sources`), single-source attribution, or a video indicator, matching the design decided earlier
- **Light/dark theme toggle** — slider in the masthead, top right, left of the settings cog. Dark is a genuine slate palette (not an inverted light theme). Persists via `localStorage`, respects system preference on first load, no flash-of-wrong-theme (set before hydration in `app.html`).
- **Admin panel** (`/admin/settings`) — disabled by default; set `ADMIN_PANEL_ENABLED=true` in `frontend/.env` to turn on the cog icon and the `/admin/*` pages (see `frontend/.env.example`). Six tabs, all wired to the mock backend's `/api/admin/*` routes:
- **Merge** — strictness slider, poll interval, hold-before-publish, follow-up thresholds, category priority (reorderable), tag dedup threshold, tag expiry
- **Sources** — list, add, enable/disable, delete RSS/API/Telegram feeds
- **Models** — AI service status, per-task model selection (embedding/image/synthesis), fetched from the mock's simulated Ollama catalog
- **Retention** — published-article and raw-item age presets, storage cap with FIFO note and usage bar
- **Tracked events** — list, create, toggle active/paused, delete
- **Connections** — the asymmetric pair: frontend→backend URL (saved to *this browser* via `localStorage`, not a backend setting) and backend→AI-service host/port (a real backend setting, saved via `/api/admin/settings`)
## Mock data
`mock-backend/data.js` has ~10 dummy articles covering: a 4-source merge with a follow-up (`art-1``art-2`, same `threadId`), single-source articles across Business/Tech, and a Local section with a mix of merged, single-source, and video items — enough variety to sanity-check every card/badge state in the design.
Timestamps are generated relative to `Date.now()` (see `hoursAgo()` in `data.js`) rather than hardcoded, so "time ago" labels stay sensible no matter when you run this.
## Connecting to the real backend later
The frontend never hardcodes `localhost:4000` — see `frontend/src/lib/config.ts`. It reads `VITE_BACKEND_URL` (set in `frontend/.env`) or a value saved via `setBackendUrl()`. Pointing this project at the real backend instead of the mock is a one-line change, not a rewrite — swap the URL in `.env` and everything else keeps working, since both servers implement the same `/api/feed`, `/api/article/:id`, `/api/tags`, `/api/events` contract from `homefeed-data-schema.md`.
## Deploying behind a reverse proxy (e.g. Nginx Proxy Manager)
Both apps are meant to run as plain, long-lived Node processes on your own host —
there's no platform-specific adapter or container packaging here, just two servers
you point a reverse proxy at.
This assumes **one public domain**, with the reverse proxy routing by path:
everything under `/api/` and `/media/` goes to the backend, everything else goes to
the frontend. (Two separate domains — e.g. `homefeed.example.com` +
`api.homefeed.example.com` — works too and is actually simpler to set up, since it
needs no path-based routing at all; skip the "Custom Locations" step below and just
point `VITE_BACKEND_URL` at the second domain instead of the first.)
### 1. Build and run the backend
```bash
cd backend
cp .env.example .env
# set FRONTEND_ORIGIN to your public domain, e.g. https://homefeed.example.com
npm install
npm run build
node --experimental-sqlite --env-file=.env dist/index.js
```
Keep this running (systemd, pm2, tmux — whatever you'd normally use). It prints a
fresh admin API key to its console/log on every start; you'll need whatever's there
each time it restarts (see `backend/README.md`).
> **`FRONTEND_ORIGIN` here and `ORIGIN` in `frontend/.env` (next step) must be the
> exact same value** — same scheme, same host, same trailing-slash-or-not. SvelteKit's
> server-side `fetch` (used to load the homepage's data) enforces real CORS during
> SSR, just like a browser would; if these two don't match character-for-character,
> every page fails with `CORS error: Incorrect 'Access-Control-Allow-Origin' header
> is present on the requested resource`. Both should be your public HTTPS domain —
> not `localhost`, not an internal port — since that's what the browser (and
> SvelteKit's own SSR fetch) actually sees as the origin.
### 2. Build and run the frontend
Now that it uses `@sveltejs/adapter-node`, `npm run build` produces a standalone
server at `build/index.js` rather than needing a specific hosting platform.
```bash
cd frontend
cp .env.example .env
# same-domain path routing (this section's assumption): point this at your one
# public domain, since /api and /media resolve there too, e.g.:
# VITE_BACKEND_URL=https://homefeed.example.com
# (separate domains instead: point this at the backend's own domain)
#
# also add to this same .env file (read at runtime, not build time):
# PORT=3000
# ORIGIN=https://homefeed.example.com
# HOST=0.0.0.0
npm install
npm run build
node --env-file=.env build/index.js
```
`ORIGIN` must match the public HTTPS URL exactly — adapter-node uses it to validate
requests and build absolute URLs; getting it wrong is the classic "works on
localhost, breaks behind the proxy" bug. Unlike `PORT`/`ORIGIN`/`HOST` (read fresh
at runtime, so `.env` changes take effect on restart), `VITE_BACKEND_URL` is baked
in at **build time** — changing your domain later means rebuilding the frontend,
not just editing `.env`.
### 3. Nginx Proxy Manager configuration
Add one Proxy Host for your domain:
- **Forward Hostname/IP**: wherever the frontend process is reachable from NPM
(`127.0.0.1` if NPM and the app run on the same host, a container/host name
otherwise) · **Forward Port**: `3000` (or whatever you set above)
- **SSL tab**: request a Let's Encrypt certificate, enable "Force SSL"
- Websockets support isn't needed — this app doesn't use any
Then, same-domain path routing only, add two **Custom Locations** on that same
Proxy Host:
| Location | Forward Hostname/IP | Forward Port |
| --------- | ------------------------------ | ------------ |
| `/api` | the backend's host | `4000` |
| `/media` | the backend's host | `4000` |
Leave the forwarded path as-is (don't strip the `/api`/`/media` prefix) — the
backend's own routes already expect them, exactly as proxied.
### Why this works without further code changes
- The frontend never assumes same-origin vs. cross-origin — every backend call goes
through `getBackendUrl()` (see `frontend/src/lib/config.ts`), producing a full
absolute URL either way.
- The admin API key (see the earlier hardening pass) is a request header, not a
cookie, so there's no cross-domain cookie/SameSite concern regardless of which
domain layout you pick.
- `backend/src/index.ts`'s CORS is still locked to `FRONTEND_ORIGIN` as a
defense-in-depth measure, even though same-domain path routing makes it moot for
actual browser traffic (the browser sees one origin the whole time, so CORS
doesn't come into play) — set it to your public domain regardless.