@@ -116,14 +116,52 @@ Lowest-risk vertical slice: stand up the data layer and convert the one 1:1 row.
116116 distinguishes language routes from other collections, so the set is editorial
117117 (mirrors migration-plan; PR4 reuses it for the language filter).
118118
119- ## PR 2b — Fact-checks grid — Phase 2
119+ ## PR 2b — Fact-checks grid — Phase 2 ✅
120120Heavier: client-component data lifting.
121- - [ ] ` lib/data/stories.ts ` — ` getFactChecks(): Story[] ` (published articles)
122- - [ ] Make ` app/fact-checks/page.tsx ` async, fetch grid data
123- - [ ] Refactor ` FactChecksExplorer ` (client) to receive ` stories ` as a prop
124- instead of importing ` lib/fact-checks-content `
125- - [ ] Keep static ` GRID ` as ` ?? fallback `
126- - [ ] Verify grid renders against staging (filters still static for now)
121+ - [x] ` lib/data/queries/fact-checks.ts ` — ` GET_FACT_CHECK_ARTICLES ` : one page of
122+ published fact-checks (newest first) + an ` _aggregate ` total count. Filters
123+ ** server-side** to the ` Debunk ` verdict via the normalized
124+ ` swp_article_metadata.swp_article_metadata_subjects ` relation, with
125+ ` $limit ` /` $offset ` . Selects the same fields as ` GET_CONTENT_LIST_ITEMS ` so
126+ ` mapStory ` consumes the rows unchanged.
127+ - [x] ` lib/data/stories.ts ` — ` getFactChecks(page): FactCheckListing ` (`{ stories,
128+ page, totalPages }` ), ` FACT_CHECKS_PAGE_SIZE = 10`. ** A fact-check is
129+ defined by carrying a ` Debunk ` verdict** , enforced in the query (the only
130+ signal separating fact-checks from homepage content-blocks + editorial test
131+ stubs that share ` route ` /` profile ` ). Route-agnostic, so it keeps working
132+ once fact-checks land on topic desks (staging desks are empty; everything
133+ sits on ` english ` ).
134+ - [x] ` app/fact-checks/page.tsx ` made async; reads ` ?page= ` , fetches that DB page
135+ with the ` ?? fallback ` pattern (static pool, paged identically).
136+ - [x] ` FactChecksExplorer ` (client) takes ` stories ` + ` page ` /` totalPages ` ;
137+ pagination navigates by ` ?page=N ` (server re-fetch) — never loads the corpus
138+ client-side.
139+ - [x] ` app/fact-checks/[desk]/page.tsx ` (PR5 placeholder) passes the static pool
140+ on a single page so it compiles + hides pagination.
141+ - [x] Verify against staging: grid renders live fact-checks (verdict badges,
142+ dates, readTime, real media). ** Server pagination proven** end-to-end by
143+ temporarily setting page size to 2 (5 items → 3 pages): each page is a
144+ distinct DB slice, ` ?page=N ` drives the fetch, boundary disables "Next".
145+ Restored to 10. tsc + biome clean; 27 Vitest tests pass.
146+
147+ ** Notes:**
148+ - ** Why server-side verdict filter (not the verified jsonb path):** the jsonb
149+ ` metadata ` column is exposed by Hasura as an opaque ` String ` (no jsonb
150+ operators), so it can't filter or paginate server-side. Pagination at scale
151+ (prod: tens of thousands of fact-checks) forces the normalized relation —
152+ which the reference prod app also uses (` GET_COLLECTION_BY_METADATA_QUERY ` ).
153+ - ** Relation vs jsonb divergence:** the relation drops ` altered-yvonne ` on
154+ staging (its ` swp_article_metadata ` is ` null ` though the jsonb carries the
155+ verdict — a staging integrity gap). All relation-matched articles still carry
156+ the jsonb verdict * name* , so ` mapStory ` 's badge is unaffected. The French
157+ "FAUX" article (no structured ` Debunk ` tag) is also absent. Both are staging
158+ artifacts; production data is consistently pipelined.
159+ - Filters remain ** static** (region/language/topic from ` fact-checks-content ` )
160+ and now operate ** client-side on the current page only** : staged chips show
161+ but ` applied ` starts empty, so the full server page is visible on load. Live
162+ ` Story ` s carry no taxonomy yet, so "Apply Filters" narrows to empty — expected.
163+ PR4 makes filtering server-side, layering filter params onto the same ` ?page= `
164+ URL the pagination already uses.
127165
128166## PR 3 — Single article — Phase 3
129167- [ ] ` lib/data/queries/ ` — single-article query (` articleService.queries.js ` ):
@@ -137,13 +175,18 @@ Heavier: client-component data lifting.
137175 working when fetching live (article lookup by slug vs desk lookup)
138176- [ ] Verify a real fact-check (e.g. ` false-equating-somalia-and-al-shabab-is-untrue ` )
139177
140- ## PR 4 — Filters & pagination — Phase 4 (depends on 2b)
178+ ## PR 4 — Filters — Phase 4 (depends on 2b)
179+ > Offset pagination already shipped in PR 2b (server-side ` limit ` /` offset ` +
180+ > ` _aggregate ` , driven by ` ?page= ` ). PR 4 makes ** filtering** server-side and
181+ > wires it onto that same URL mechanism.
141182- [ ] Map filter dimensions to real taxonomy: ` region ` → ` subject[countries] ` ,
142183 ` topic ` → ` subject[01harm] ` /route, ` language ` → article language/route
184+ (extend ` mapStory ` to populate ` topic ` /` region ` /` language ` on ` Story ` )
143185- [ ] Derive filter option lists from live data (replace hardcoded ` REGIONS ` /
144186 ` LANGUAGES ` /` TOPICS ` in ` fact-checks-content ` )
145- - [ ] Offset pagination (cf. ` collectionService ` + ` _aggregate ` for counts)
146- - [ ] Decide client-side filter vs server query params; verify filtering works
187+ - [ ] Push selected filters into the query ` where ` (server-side) + reflect them
188+ in the URL alongside ` ?page= ` ; reset to page 1 on filter change. Replace
189+ the interim client-side, current-page-only filtering in ` FactChecksExplorer ` .
147190
148191## PR 5 — Desk pages — Phase 5 (depends on 3 for route dispatch)
149192- [ ] ` lib/data/stories.ts ` — ` getByDesk(slug): Story[] ` (by route collection)
0 commit comments