Add genre and language metadata to storylines#266
Conversation
- Migration 00014: add genre (nullable) and language (default English) columns with indexes - Types: add genre/language to Storyline Row/Insert/Update - Constants: lib/genres.ts with GENRES and LANGUAGES arrays - Create form: genre (required) and language (default English) dropdowns - usePublish: forward optional metadata to indexer POST body - Storyline indexer: accept and store genre/language - Discovery page: genre and language filter dropdowns (all four tabs) - StoryCard: display genre badge + non-English language badge - Story detail: display genre + language in header metadata Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Summary
The metadata flow and display updates are mostly in place, but the discovery filters do not satisfy the ticket requirement across all four tabs.
Findings
- [medium]
genre/languagefilters are ignored on thetrendingandrisingtabs.queryTab()applies the new filters only to the direct Supabase queries fornewandcompleted, whiletrendingandrisingstill delegate straight togetTrendingStorylines()/getRisingStorylines()with only the writer-type filter. This means the UI shows genre/language dropdowns, but those tabs return unfiltered results.- File:
src/app/page.tsx:186 - Suggestion: thread
genreandlangthrough the ranking queries as well, or apply equivalent filtering before returning those ranked results.
- File:
Decision
Request changes. The ticket explicitly requires genre/language filtering on all four discovery tabs, so the trending and rising paths need to honor the new filters before merge.
project7-interns
left a comment
There was a problem hiding this comment.
REQUEST CHANGES — one blocking issue.
Blocking: No server-side validation on genre/language in the storyline indexer.
src/app/api/index/storyline/route.ts:31-32 accepts any string from the POST body:
const genre = (body.genre as string | undefined) || null;
const language = (body.language as string | undefined) || "English";The create form validates on the client (genre required, values from GENRES array), but the API endpoint stores whatever is POSTed. A direct POST to /api/index/storyline can inject arbitrary genre/language strings, polluting the database and breaking filter queries that assume values match the GENRES/LANGUAGES constants.
Fix: validate against the allowed values server-side:
import { GENRES, LANGUAGES } from "../../../../lib/genres";
const rawGenre = body.genre as string | undefined;
const genre = rawGenre && (GENRES as readonly string[]).includes(rawGenre) ? rawGenre : null;
const rawLang = body.language as string | undefined;
const language = rawLang && (LANGUAGES as readonly string[]).includes(rawLang) ? rawLang : "English";Non-blocking observations:
-
...opts.metadataspread is untyped —usePublishspreads arbitrary keys into the indexer body. Consider typingmetadataas{ genre: string; language: string }instead ofRecord<string, string>to prevent accidental field injection. -
Overall structure is clean: migration is correct (nullable genre, defaulted language, indexes),
applyFiltershelper DRYs up the query logic nicely, genre/language display is consistent across StoryCard and story detail, and the filter dropdowns correctly preserve other URL params.
Add genre/lang params to fetchCandidatesAndRatings, getTrendingStorylines, and getRisingStorylines. Discovery page now passes filters to all four tabs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Summary
The trending/rising filter path is fixed, but the storyline indexer still trusts client-supplied metadata without validating it against the allowed genre/language sets.
Findings
- [medium]
/api/index/storylinestores arbitrarygenreandlanguagestrings from the request body. The create form constrains values in the UI, but a direct POST can still write unsupported values intostorylines, which then break the intended filter/display invariants. This should be validated server-side againstGENRES/LANGUAGESbefore upsert.- File:
src/app/api/index/storyline/route.ts:31 - Suggestion: reject invalid values with
400, or coerce unknowngenretonulland unknownlanguagetoEnglishusing the shared constants.
- File:
Decision
Request changes. The filtering implementation is now complete across all four tabs, but the server needs to validate metadata before persisting it.
Coerce unknown genre to null, unknown language to English. Prevents arbitrary metadata via direct POST. Uses shared GENRES/LANGUAGES arrays. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
APPROVE
Server-side validation fix confirmed: indexer now validates genre/language against the shared GENRES/LANGUAGES arrays, coercing unknown values to null/"English". This matches the approach I suggested — safe at the API boundary without being overly strict (coerce rather than reject).
Both my blocking issue and T2a's equivalent finding are resolved. The full implementation across migration, types, create form, indexer, discovery filters, and display is solid.
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Summary
The follow-up patches close the remaining gaps in the metadata rollout. Genre/language filters now apply across all four discovery tabs, and the storyline indexer validates client-supplied metadata before persisting it.
Findings
- [info] No blocking issues found in the updated patch.
Decision
Approve. lib/ranking.ts now threads genre/language filters through trending and rising, and /api/index/storyline constrains metadata to the shared GENRES / LANGUAGES sets with safe coercion for invalid inputs. CI was still pending when reviewed.
Summary
00014_genre_language.sql: addsgenre(nullable) andlanguage(default 'English') columns with indexeslib/supabase.tsupdated with genre/language fieldslib/genres.tswith GENRES (21 options) and LANGUAGES (11 options) arraysmetadatato indexer POST bodyFixes #265
Test plan
npm run typecheckpasses🤖 Generated with Claude Code