Session 29: Content generation templates — slate threads, POTD, recaps, matchup previews (1660 tests)

This commit is contained in:
Kev
2026-06-13 21:30:57 -04:00
parent c48aecd510
commit 927c4a5c65
11 changed files with 1063 additions and 1 deletions
+102
View File
@@ -0,0 +1,102 @@
'use strict';
/**
* /api/content (Session 29)
*
* Structured content objects generated from live VYNDR data, degrading
* gracefully by data level. Read-only, zero-credit (reads cached data).
* `?format=text` additionally returns post-ready plain text.
*
* GET /api/content/slate/:sport → daily slate thread
* GET /api/content/potd/:sport → prop (or game) of the day
* GET /api/content/recap/:sport → results recap (needs resolved grades)
* GET /api/content/preview/:sport/:gameId → matchup preview for one game
*/
const express = require('express');
const template = require('../services/contentTemplateService');
const formatter = require('../services/contentFormatter');
const { cacheGet } = require('../utils/redis');
const router = express.Router();
const MISSION_HEADER = { 'X-VYNDR-Mission': 'Every post is a free ad' };
const SUPPORTED = new Set(['nba', 'wnba', 'mlb', 'soccer', 'nfl', 'nhl']);
function guard(req, res) {
const sport = String(req.params.sport || '').toLowerCase();
if (!SUPPORTED.has(sport)) {
res.status(404).set(MISSION_HEADER).json({ error: `No content for sport: ${sport}` });
return null;
}
return sport;
}
router.get('/slate/:sport', async (req, res) => {
const sport = guard(req, res);
if (!sport) return undefined;
try {
const data = await template.collectSlateData(sport);
const thread = template.generateSlateThread(sport, data);
const body = { ...thread };
if (req.query.format === 'text') body.text = formatter.formatSlateThread(thread);
return res.set(MISSION_HEADER).json(body);
} catch (err) {
console.error(`[content/slate/${sport}]`, err.message);
return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' });
}
});
router.get('/potd/:sport', async (req, res) => {
const sport = guard(req, res);
if (!sport) return undefined;
try {
const data = await template.collectSlateData(sport);
const potd = template.generatePOTD(sport, data);
const body = { ...potd };
if (req.query.format === 'text') body.text = formatter.formatPOTD(potd);
return res.set(MISSION_HEADER).json(body);
} catch (err) {
console.error(`[content/potd/${sport}]`, err.message);
return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' });
}
});
router.get('/recap/:sport', async (req, res) => {
const sport = guard(req, res);
if (!sport) return undefined;
try {
// Resolved grades land in a daily cache when the grading flow settles.
const utc = new Date().toISOString().split('T')[0];
const cache = (await cacheGet(`results:${sport}:${utc}`)) ?? (await cacheGet(`results:${sport}`));
const resolved = Array.isArray(cache) ? cache : Array.isArray(cache?.grades) ? cache.grades : [];
const recap = template.generateResultsRecap(sport, resolved);
const body = { ...recap };
if (req.query.format === 'text') body.text = formatter.formatRecap(recap);
return res.set(MISSION_HEADER).json(body);
} catch (err) {
console.error(`[content/recap/${sport}]`, err.message);
return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' });
}
});
router.get('/preview/:sport/:gameId', async (req, res) => {
const sport = guard(req, res);
if (!sport) return undefined;
const { gameId } = req.params;
try {
const data = await template.collectSlateData(sport);
const game = (data.schedule || []).find((g) => String(g.id) === String(gameId));
if (!game) {
return res.status(404).set(MISSION_HEADER).json({ error: 'Game not on today\'s schedule.' });
}
const lines = template.__internals.findGameLinesFor(game, data.gameLines);
const preview = template.generateMatchupPreview(game, lines, data.streaks);
return res.set(MISSION_HEADER).json(preview);
} catch (err) {
console.error(`[content/preview/${sport}]`, err.message);
return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' });
}
});
module.exports = router;