---
title: Combine coachspeak into the news surface
type: task
status: completed
description: >-
  Fold /coachspeak into a /news section with sub-path modes and one menu entry, add coach claims to
  the player news tab, match the team filter on the claimed player, and restyle toward the league
  homepage.
base_uri: user:task/league/combine-coachspeak-into-news-surface.md
created_at: '2026-10-17T22:10:05.691Z'
entity_id: 926620a4-d237-405a-8d1a-fac4c810a4be
finished_at: '2026-10-18T00:30:46.139Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[█████████████████████-████-██████-███-█████-███████████]]
  - follows [[██████████████████████-██████████-█████████-██████-████████]]
  - relates [[██████████████████████-█████████-███████-██████████████████]]
started_at: '2026-10-17T23:09:45.303Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-18T00:30:54.472Z'
visibility_analyzed_at: '2026-11-05T23:48:35.026Z'
---

Fold xo's `/coachspeak` page into the `/news` surface as one news section with sub-path modes under a single navigation entry, put coach claims on the player news tab, and restyle the section toward the league homepage. All design rulings were made by the operator on 2026-10-17.

## Overview

- One news section at `/news` with three modes: the stream (`/news`), coach quotes (`/news/coachspeak`), and community recommendations (`/news/community-recommendations`, built by another lane). One menu entry covers all of them.
- A "Coaches on this player" block on the player news tab, so a player's coach claims appear where readers look for that player.
- The coachspeak team filter matches the claimed player's current club, not the document's clubs.
- The section's heads, controls and type move to the league homepage's language, with article headlines and quote text in the homepage serif.
- Impact: a reader asking about a player sees what coaches said about him beside the news. Today about 70% of actionable coach claims never reach that player's view.
- Ordering: this follows [[██████████████████████-██████████-█████████-██████-████████]]. It gives [[█████████████████████-████-██████-███-█████-███████████]] its mount point and the shared player-quote state. It hosts the community recommendations page from [[██████████████████████-█████████-███████-██████████████████]].

## Background

- Both pages are top-level public routes (`app/views/routes.js:160`, `:163`) with side-by-side menu links (`app/views/components/menu/menu.js:284-285`), and each wraps itself in `PageLayout body scroll`.
- `/news` keeps `topic` and `player` in the URL inline in `app/views/pages/news/news.js:225-262` and preserves foreign query keys. `/coachspeak` keeps `pid`, `team`, `direction`, `kind`, `all` in `app/core/coachspeak/coachspeak-view.mjs` and replaces the whole query on navigate (`app/views/pages/coachspeak/coachspeak.js:100`).
- The coachspeak slice holds one view. The player news tab reads items by item player tag (`app/core/player-news/build-player-news-request-params.mjs:56-71`) and shows those items' notes, but no coach quotes.
- Item-grain player filters read only `feed_items.players` or `focused_players` (`extension/content-feed-query/lib/build-feed-query.mjs:738`). 161 of 536 actionable claims sit on an item tagged with the claimed player (content-feed query, 2026-10-17).
- The note query's team filter tests document clubs (`extension/content-feed-query/lib/build-note-query.mjs:124-126`). The item query already expands teams to current roster pids through `expand_nfl_team_roster` in `content-feed-query-route.mjs:194-201`.
- Styling: the news files use the homepage's tokens (`app/styles/prose-form.styl`) but not its section mixins (`prose_section_band`, `prose_section_head`, `prose_section_label`, `prose_section_more`, `prose_gloss`). Their controls are mono uppercase and hover to the accent. The player news tab uses app-chrome tokens and a literal mono font. STYLE.md:188 declines the serif for `/news`.
- Tests: `test/app.route-registration.spec.mjs` parses `<Route path='...'>` from `routes.js` and requires a page-routes entry for every non-redirect route. Nested routes with absolute child paths pass, as `/leagues/:lid` does.

### Assumptions

- Per-player claim volume is small (median 2, 90th percentile 8 across the backfill), so the player tab can show every claim kind (checked: content-feed query, 2026-10-17).
- `NavLink to='/news'` stays active on `/news/*` because react-router matches descendants unless `end` is set (unchecked; verified at the menu step).
- The infinite-scroll observer finds its root by walking up to the scroll container (`news.js:81`), so nesting inside a layout that owns `PageLayout` does not change the root (unchecked; verified at the layout step).
- `/api/coachspeak` without claim filters matches `document_notes.players`, and with claim filters it also requires the claim's pid (`build-note-query.mjs:121-142`, read).
- The roster snapshot's present-tense club is the right reading for a seven- or thirty-day quote window (accepted trade-off: a player traded inside the window lists under his new club).

## Design

- **Routes.** `<Route path='/news' element={<NewsSectionPage />}>` with an index child (the stream), `path='/news/coachspeak'`, and later `path='/news/community-recommendations'`. Child paths are written in full so the route spec keeps parsing. `/coachspeak` becomes `<Navigate to={`/news/coachspeak${location.search}`} replace />`, rewriting `pid` to `player`.
- **Layout.** New `app/views/pages/news-section/` owns `PageLayout body scroll`, the 2px ink masthead rule, the mode switch and an `<Outlet />`. The stream and coachspeak pages become mode bodies without their own `PageLayout`. The answer box mounts at the top of the layout body under the mode switch, and the answer lane adds that line. No placeholder is built.
- **Mode switch.** Links named News, Coachspeak and Recommendations. Each carries the current `player` query key across modes and drops the others.
- **Query keys.** Coachspeak renames `pid` to `player`. `CoachspeakQuote` links to `/news/coachspeak?player=`.
- **Shared quote state.** The coachspeak slice holds results per `coachspeak_view_key` (a map of views), and the load and next actions take the view. The page and the player tab each request their own view. This is the reusable piece the answer lane consumes.
- **Player tab block.** `SelectedPlayerCoachspeak` renders above the day-grouped items. It requests `{player: pid, show_all: true}` over 30 days, shows the three latest quotes, links "See all" to `/news/coachspeak?player=<pid>`, and renders nothing when empty. The view gains a `window_days` field, defaulting to the route's seven, so the request carries `start_date`.
- **Team filter.** The note route expands `nfl_team_abbreviations` to current roster pids. A note matches when one of its `players` is on that roster, or when it has no players and its document is tagged with the club. The league route and client are unchanged.
- **Restyle.**
  - Section heads use the homepage mixins.
  - A new `prose_toggle()` mixin in `app/styles/prose-form.styl` (Inter 11px 600 uppercase, muted at rest, ink with underline when active or hovered) replaces the mono word-buttons in the mode switch, topic row, live-band mode, coachspeak filters and team select.
  - The coachspeak `h1` and lede give way to a gloss.
  - Injury group heads take the rule above the label.
  - The player news tab moves to the prose tokens.
  - Article headlines and quote text use `$prose_serif`. Bylines, meta and controls stay sans.
  - The rows and the column measure are otherwise unchanged. There is no rail.
- **Naming check.** `NewsSectionPage`, `news-section`, `SelectedPlayerCoachspeak`, `prose_toggle` and `window_days` follow the repo's full-word snake_case and kebab-case directory rules. No schema columns are added.
- **Knowledge Base Impact.**
  - STYLE.md /news rulings (serif at :188, section head and rule side at :274-289, the toggle treatment) are rewritten in place.
  - `docs/guides/spa.md` gains the section layout and mode routes.
  - The coachspeak passage of `█████████████████████████████████████████████████` and the note team filter in `text/base/content-feed-query-api.md` are updated.
  - No new tags or `ABOUT.md` edits.
- **Scope.** One task, no scratch area, no orchestrator.

## Notes

- Some real claims are labelled `praise`. The player tab shows all kinds, so nothing is hidden there.
- Four press conferences have no quotes after re-summarizing. That is upstream and owned by `█████████████████`.
- Lane bounds: `███████████████████████████████` owns the answer endpoint and component and adds one mount line each to the layout and the player tab. `████████████████████████████████████` owns the recommendations page body. This task owns the section structure, navigation and styling.
- The team-filter change lives in user-base and goes live on the next base-api restart.

## Plan

### Section structure

- [x] **Add the news section layout**
  - Files: `app/views/pages/news-section/{index.js,news-section.js,news-section.styl}`
  - Purpose: one page shell for every news mode.
  - Changes: `PageLayout body scroll` holding the masthead rule, the mode switch and `<Outlet />`. Mode links preserve `player`.
  - Verify: `yarn dev:live`. `/news` and `/news/coachspeak` render inside one shell. Switching modes with `?player=` set keeps the player.
- [x] **Turn the stream and coachspeak pages into mode bodies**
  - Files: `app/views/pages/news/news.js`, `app/views/pages/coachspeak/coachspeak.js`
  - Changes: return the body instead of wrapping it in `PageLayout`. Coachspeak navigate preserves foreign keys.
  - Verify: infinite scroll on `/news` still pages (sentinel fires). `yarn test` passes.
- [x] **Nest the routes and redirect `/coachspeak`**
  - File: `app/views/routes.js`
  - Changes: the layout route with index and coachspeak children. `/coachspeak` becomes a `Navigate` that maps `pid` to `player`.
  - Verify: `yarn test` (`test/app.route-registration.spec.mjs`). `/coachspeak?pid=X` lands on `/news/coachspeak?player=X`.
- [x] **Register page meta**
  - File: `libs-shared/page-routes.mjs`
  - Changes: a `/news/coachspeak` entry replaces `/coachspeak`.
  - Verify: `yarn test` (page-meta specs). `curl -s localhost/news/coachspeak` carries the Coachspeak title.
- [x] **Nest the community recommendations mode** (after branch `community-content-recommendations` merges)
  - Files: `app/views/routes.js`, `app/views/pages/community-content-recommendations/community-content-recommendations-page.js` (deleted), `app/views/pages/community-content-recommendations/community-recommendations.styl`
  - Changes: make `/news/community-recommendations` a child of the layout route with the body component as its element, delete the page wrapper, and replace the `__toggle` and `__action` blocks with `prose_toggle()`.
  - Verify: `yarn test` (route registration). `/news/community-recommendations?medium=podcast` renders inside the shell with the mode switch on Recommendations.
- [x] **Drop the Coachspeak menu link**
  - File: `app/views/components/menu/menu.js`
  - Verify: News is highlighted on `/news/coachspeak` in `yarn dev:live`.

### Coachspeak state and links

- [x] **Rename `pid` to `player` and add `window_days`**
  - File: `app/core/coachspeak/coachspeak-view.mjs`
  - Changes: the URL key `player`, and `window_days` mapped to `start_date` in the request params.
  - Verify: `test/app.coachspeak-slice.spec.mjs` updated and passing.
- [x] **Hold views by key**
  - Files: `app/core/coachspeak/{actions.js,reducer.js,sagas.js,selectors.js}`, `app/views/pages/coachspeak/index.js`
  - Changes: state is a map of `coachspeak_view_key` to `{items, cursor, is_pending, has_loaded, failed, degraded}`. Actions and the selector take the view.
  - Verify: a spec loads two views and asserts neither overwrites the other. `yarn test`.
- [x] **Point quote links at the mode**
  - File: `app/views/components/coachspeak-quote/coachspeak-quote.js`
  - Changes: `/news/coachspeak?player=`.
  - Verify: `grep -rn "/coachspeak?pid" app` returns nothing.

### Player news tab

- [x] **Add the coaches block**
  - Files: `app/views/components/selected-player-coachspeak/{index.js,selected-player-coachspeak.js,selected-player-coachspeak.styl}`, `app/views/components/selected-player-news/selected-player-news.js`
  - Changes: request `{player: pid, show_all: true, window_days: 30}`, render three `CoachspeakQuote`s and "See all", render nothing when empty.
  - Verify: `yarn dev:live` on a player with quotes (pick one from `select cl->>'pid', count(*) from document_notes, jsonb_array_elements(player_claims) cl group by 1 order by 2 desc limit 5`) shows the block. A player with none shows no block.

### Team filter by claimed player

- [x] **Match notes on the club's current roster**
  - Files: `extension/content-feed-query/lib/{content-feed-query-route.mjs,normalize-note-query.mjs,build-note-query.mjs}`, `extension/content-feed-query/tests/{build-note-query.test.mjs,content-feed-note-route.test.mjs}`
  - Changes: expand `nfl_team_abbreviations` to roster pids in the `/notes` handler (503 when the snapshot is unavailable, as the item route does). Build `(n.players ?| roster_pids OR (players empty AND document teams && teams))`.
  - Verify: the extension tests pass. After the base-api restart, `/api/coachspeak?nfl_team_abbreviations=CHI&claim_kinds=usage,availability,job_security,praise,none` includes a quote about a Bears player from a non-Chicago document. Baseline query: `select count(*) from document_notes dn, jsonb_array_elements(dn.player_claims) cl join feed_item_documents fid on fid.document_id=dn.document_id join feed_items fi on fi.item_id=fid.item_id where cl->>'pid'='<bears pid>' and not fi.nfl_teams ? 'CHI'` is greater than 0.

### Restyle

- [x] **Add `prose_toggle()`**
  - File: `app/styles/prose-form.styl`
  - Verify: `yarn build` compiles.
- [x] **Restyle the stream mode**
  - Files: `app/views/pages/news/news.styl`, `app/views/components/news-head/news-head.styl`, `app/views/components/news-injuries/news-injuries.styl`, `app/views/components/news-item-compact/news-item-compact.styl`, `app/views/components/news-item-article/news-item-article.styl`
  - Changes: band heads use the section mixins. The topic row, live-band mode and band-more use `prose_toggle` and `prose_section_more`. The injury group rule moves above the label. Headlines use `$prose_serif`.
  - Verify: before and after screenshots of `/news`, the injury view and the league homepage side by side in `yarn dev:live`.
- [x] **Restyle coachspeak**
  - Files: `app/views/pages/coachspeak/{coachspeak.js,coachspeak.styl}`, `app/views/components/coachspeak-quote/coachspeak-quote.styl`
  - Changes: drop the `h1` and lede for a gloss. Filters use `prose_toggle`. Quote text uses `$prose_serif`.
  - Verify: screenshot of `/news/coachspeak` beside the homepage.
- [x] **Restyle the player news tab**
  - Files: `app/views/components/selected-player-news/selected-player-news.styl`, `app/views/components/news-item-spoken/news-item-spoken.styl`
  - Changes: prose tokens replace `$textColor`, `$accentColor` and the literal mono font. Spoken quote text uses the serif.
  - Verify: `grep -n "textColor\|accentColor\|'IBM Plex Mono'" app/views/components/selected-player-news/selected-player-news.styl` returns nothing. A screenshot of the tab.

### Knowledge Base Updates

- [x] **Rewrite the /news rulings in STYLE.md**
  - Changes: serif for headlines and quotes, section heads by the homepage mixins, `prose_toggle` for controls.
  - Verify: `grep -n "declined\|still stands for \`/news\`" STYLE.md` returns no stale serif ruling.
- [x] **Document the section in `docs/guides/spa.md`**
  - Verify: `grep -n "news-section\|/news/coachspeak" docs/guides/spa.md` hits.
- [x] **Update the user-base docs for the team filter and page move**
  - Files: `█████████████████████████████████████████████████`, `text/base/content-feed-query-api.md`
  - Verify: `grep -rn "/coachspeak?\|document-scoped" █████████████████████████████████████████████████ text/base/content-feed-query-api.md` shows only current wording.

### Testing and Validation

- [x] **Full suite and live check**
  - `yarn test` in league, the extension tests in user-base, and a `yarn dev:live` walk of `/news`, `/news/coachspeak`, `/coachspeak?pid=X` and a player tab.
  - Population check: across the five players with the most claims, the player tab block shows at least one quote for each, and its count over 30 days matches `GET /api/coachspeak?player_ids=<pid>&start_date=<30 days ago>`. This catches a block that renders but filters to nothing.
