Keep prod health checks and route fallbacks from failing on stale API entries

Add a Bun health script that exercises top-level, dynamic, and representative video routes against one or more hosts so prod regressions are visible from a single command.

Device pages now fall back to the bundled device list when the external API misses a slug, and orphaned tv slugs redirect to /benchmarks instead of returning a 500. Video fallback logic reuses the existing YouTube-to-listing builder so route reconstruction stays aligned with the current build logic.

Constraint: The external API host can lag behind the frontend build and omit per-slug JSON files that public routes still expect
Rejected: Import the generated video list directly | static/video-list.json is too large for a safe SSR fallback
Rejected: Leave missing tv routes as 500s | a stale public URL should degrade to a useful redirect instead of breaking the request
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Keep route fallbacks tied to build-time artifacts from the same repo so frontend and fallback data stay in sync
Tested: bun scripts/health http://127.0.0.1:4322; vitest ./test/prebuild/config-node.test.js ./test/prebuild/site-listings.test.js; pnpm run netlify-build
Not-tested: live production deploy before push
This commit is contained in:
ThatGuySam 2026-04-06 10:51:49 -05:00
parent 820e495d2d
commit 6cfbfbf530
7 changed files with 224 additions and 7 deletions

View file

@ -25,7 +25,10 @@ const defaultFetchMethod = async function (...args) {
return axios(...args)
.then( response => response.data )
.catch( error => {
console.error( error )
if ( error?.response?.status !== 404 ) {
console.error( error )
}
throw error
})
}

View file

@ -33,6 +33,10 @@ const videoFeaturesApp = function (app, video) {
return false
}
export function makeVideoSlug ( title, videoId ) {
return makeSlug( `${ title }-i-${ videoId }` )
}
const generateVideoTags = function ( video ) {
const tags = {
'benchmark': {
@ -129,7 +133,7 @@ const makeThumbnailData = function ( thumbnails, widthLimit = null ) {
}
}
async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
export async function buildVideoListingFromFetchedVideo ( fetchedVideo, videoId, applist ) {
// Skip private videos
if (fetchedVideo.title === 'Private video') return
@ -138,7 +142,7 @@ async function handleFetchedVideo ( fetchedVideo, videoId, applist ) {
if (fetchedVideo.title === 'Deleted video') return
// Build video slug
const slug = makeSlug( `${fetchedVideo.title}-i-${videoId}` )
const slug = makeVideoSlug( fetchedVideo.title, videoId )
const appLinks = []
// Generate new tag set based on api data
@ -200,8 +204,7 @@ export default async function ( applist ) {
.withConcurrency(1000)
.for( Object.entries( fetchedVideos ) )
.process(async ( [ videoId, fetchedVideo ], index, pool ) => {
const mappedVideo = await handleFetchedVideo ( fetchedVideo, videoId, applist )
const mappedVideo = await buildVideoListingFromFetchedVideo( fetchedVideo, videoId, applist )
// Skip if this video is not an object
if ( Object( mappedVideo ) !== mappedVideo ) return

47
helpers/site-listings.js Normal file
View file

@ -0,0 +1,47 @@
import fs from 'fs-extra'
import path from 'path'
import { fileURLToPath } from 'url'
import {
buildVideoListingFromFetchedVideo,
makeVideoSlug
} from '~/helpers/build-video-list.js'
import { youtubeVideoPath } from '~/helpers/api/youtube/build.js'
const currentModuleDirectory = path.dirname( fileURLToPath( import.meta.url ) )
const appListPath = path.join( currentModuleDirectory, '../static/app-list.json' )
const gameListPath = path.join( currentModuleDirectory, '../static/game-list.json' )
const deviceListPath = path.join( currentModuleDirectory, '../static/device-list.json' )
const trailingCommaPattern = /,\s*([\]}])/g
function parseGeneratedJsonFile ( filePath ) {
const fileContents = fs.readFileSync( filePath, 'utf8' )
return JSON.parse( fileContents.replace( trailingCommaPattern, '$1' ) )
}
export function getDeviceListingBySlug ( slug ) {
const deviceList = parseGeneratedJsonFile( deviceListPath )
return deviceList.find( device => device.slug === slug ) || null
}
function getAllVideoAppsList () {
return [
...parseGeneratedJsonFile( appListPath ),
...parseGeneratedJsonFile( gameListPath )
]
}
export async function getVideoListingBySlug ( slug ) {
const fetchedVideos = await fs.readJson( youtubeVideoPath )
const allVideoAppsList = getAllVideoAppsList()
for ( const [ videoId, fetchedVideo ] of Object.entries( fetchedVideos ) ) {
if ( makeVideoSlug( fetchedVideo.title, videoId ) !== slug ) continue
return await buildVideoListingFromFetchedVideo( fetchedVideo, videoId, allVideoAppsList )
}
return null
}