internetarchive / internetarchive/openlibrary
feat(community): Weekly Prompt homepage widget — staff-curated book discovery
- Dominant language
- Python
- Stars
- 6.7k
- Forks
- 2k
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 138
Description
## Summary
Open Library's homepage currently shows a wall of \"trending\" books that aren't genuinely trending. We want to jumpstart the community/following ecosystem by surfacing **active humans** and creating mechanisms for patrons to discover books *and each other*.
The first step is a **Weekly Prompt** widget — a staff-curated question that patrons can engage with by nominating and voting on books. This brings real, time-stamped human activity to the homepage without requiring a full social graph.
---
## Background
This feature builds on [TheBestBookOn.com](https://github.com/Open-Book-Genome-Project/TheBestBookOn.com) (TBBO), an existing Open Book Genome Project service already keyed to OL usernames and OL work/edition keys. We are **not** porting TBBO wholesale — we're migrating the concept and data model into Open Library as a proper first-class feature, starting fresh.
We've been working on prompts in some form since 2017 (predating implementations like Hardcover's). This issue is the first step toward making that work visible and usable.
---
## Design Principles
- **Mobile-first** — the widget must be fully usable on a phone before considering desktop
- **Flexible prompt format** — not every prompt is "The Best Book on X is:". Valid prompts include "What is Stephen King's best novel?" or "Non-fiction books that expanded your understanding of the world". The schema must not enforce a formula.
- **Staff-curated, patron-engaged** — for now, only staff creates prompts. Patrons nominate and vote on books.
- **Data separation** — prompt data lives in a separate DB (not OL's core Infobase). Clear boundary: no \"see all your nominations\", no \"see all your favorites\" in OL — that belongs to TBBO or a future microservice.
- **Lit component** — the homepage widget must be a Lit web component following OL's existing component patterns.
---
## Data Model
This is the canonical schema for both this issue (Lit component) and the backend follow-up:
### `Prompt`
| Field | Type | Notes |
|-------|------|-------|
| id | Integer PK | |
| title | String | The prompt question |
| description | String | Context / framing |
| creator_username | String | OL username of staff creator |
| created_at | DateTime | Used to determine \"current\" prompt |
**Current prompt rule:** most recent `created_at ≤ now()` is the active prompt. No flag needed — staff simply creates a new prompt each week and it becomes active.
### `Submission` (nomination)
| Field | Type | Notes |
|-------|------|-------|
| id | Integer PK | |
| prompt_id | FK → Prompt | |
| openlibrary_edition_key | String | e.g. `OL12345M` |
| submitter_username | String | OL username |
| title | String | Cached book title |
| cover_id | Integer | OL cover ID |
| ebook_access | String | `borrowable` / `printdisabled` / `public` / `no_ebook` |
| created_at | DateTime | |
### `Vote`
| Field | Type | Notes |
|-------|------|-------|
| id | Integer PK | |
| submission_id | FK → Submission | |
| voter_username | String | OL username |
| value | Integer | +1 or -1 only |
| comment | Text | Optional justification |
| created_at | DateTime | |
| — | Unique(voter_username, submission_id) | One vote per user per submission |
### `Favorite`
| Field | Type | Notes |
|-------|------|-------|
| id | Integer PK | |
| username | String | OL username |
| prompt_id | FK → Prompt | |
| created_at | DateTime | |
| — | Unique(username, prompt_id) | |
### `Tag` + `SubmissionTag`
Freeform string tags on submissions. Many-to-many via association table.
---
## MVP Scope (this issue)
**In scope — `OLWeeklyPrompt` Lit component:**
- [ ] Create `openlibrary/components/lit/OLWeeklyPrompt.js`
- [ ] Register in `openlibrary/components/lit/index.js`
- [ ] Create homepage sub-template `openlibrary/templates/home/weekly_prompt.html`
- [ ] Integrate into `openlibrary/templates/home/index.html` (above the fold, between welcome and trending carousel)
- [ ] Component accepts data via properties/attributes (stubbed data for MVP, real API in follow-up)
- [ ] Mobile-first layout: horizontal scroll book strip on mobile, grid on desktop (≥ 640px)
- [ ] Vite build compiles with 0 errors; renders correctly at 375px and 1280px viewports
**Component properties:**
```js
promptTitle // String — the prompt question
promptDesc // String — "Staff pick · Week of June 2" etc.
nominations // Array — [{coverUrl, title, score, workKey}]
totalVoters // Number
username // String — current OL username or empty (for auth-gate UI)
```
---
## Out of Scope (follow-up issues)
- Backend FastAPI router + SQLite schema (`openlibrary/fastapi/prompts.py`) — follow-up
- History page at `/prompts` showing all historical weekly prompts — follow-up
- Staff prompt creation UI — follow-up
- \"See all your nominations\" / \"See all your favorites\" — intentionally out of OL scope; belongs in TBBO/microservice
- Data migration from existing TBBO database — follow-up or never (start fresh)
---
## Related / Prior Art
- [TheBestBookOn.com](https://github.com/Open-Book-Genome-Project/TheBestBookOn.com) — existing implementation this is based on
- `openlibrary/core/bestbook.py` — earlier OL award system (simpler schema, no voting/favorites; inform but don't modify in this PR)
- Existing Lit components to follow as patterns: `OLChip.js`, `OLReadMore.js`
---
## Success Criteria
- [ ] `make lit-components` compiles with 0 webpack/Vite errors
- [ ] `http://localhost:8080` returns HTTP 200 after `docker compose restart web`
- [ ] At 375px viewport: prompt title visible, book strip horizontally scrollable, CTA button reachable without vertical scroll
- [ ] At 1280px viewport: two-column layout (prompt text + nomination grid)
- [ ] No JS console errors on homepage load
- [ ] Component degrades gracefully when `nominations` is empty (shows CTA with "Be the first to nominate a book")
Contributor guide
Assessment
This issue has not been assessed yet.