diff --git a/netlify/edge-functions/yt/index.mjs b/netlify/edge-functions/yt/index.mjs new file mode 100644 index 000000000..a504fe436 --- /dev/null +++ b/netlify/edge-functions/yt/index.mjs @@ -0,0 +1,28 @@ +// The JSON lookup gets generated at build time, do not manually edit. See `node-scripts/generate-youtube-redirects.js` +import redirects from './redirects.json' assert { type: 'json' }; + +export default async (request) => { + const url = new URL(request.url); + const youtubeId = url.pathname.split('/')[2]; + + const headers = { + 'Cache-Control': 'public, max-age=86400' // 24h + }; + + if (redirects[youtubeId]) { + return new Response(null, { + status: 302, + headers: { + ...headers, + Location: redirects[youtubeId] + } + }); + } + + return new Response('Not Found', { + status: 404, + headers + }); +}; + +export const config = { path: '/yt/:youtubeId' }; diff --git a/netlify/edge-functions/yt/redirects.json b/netlify/edge-functions/yt/redirects.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/netlify/edge-functions/yt/redirects.json @@ -0,0 +1 @@ +{} diff --git a/node-scripts/generate-youtube-redirects.js b/node-scripts/generate-youtube-redirects.js new file mode 100644 index 000000000..d74c7643a --- /dev/null +++ b/node-scripts/generate-youtube-redirects.js @@ -0,0 +1,57 @@ +const fs = require('node:fs'); +const { paths, toSlug } = require('../content-testing/content'); + +// Generates a JSON file that maps YouTube IDs to their challenge or track URL +// Priority: challenge > canonical track > track + +const redirects = {}; + +for (const path of paths.challenges) { + const slug = toSlug.videosAndChallenges(path); + const video = JSON.parse(fs.readFileSync(path)); + const parts = video.parts ?? [video]; + + parts.forEach((part, i) => { + const partAnchor = i > 0 ? `#part-${i + 1}` : ''; + redirects[part.videoId] = `/${slug}${partAnchor}`; + }); +} + +const slugToVideo = new Map(); +for (const path of paths.videos) { + const slug = toSlug.videosAndChallenges(path); + const video = JSON.parse(fs.readFileSync(path)); + slugToVideo.set(slug, video); +} + +for (const path of paths.tracks) { + const trackSlug = toSlug.tracks(path); + const track = JSON.parse(fs.readFileSync(path)); + + const chaptersOrVideos = + track.videos ?? track.chapters.flatMap((c) => c.videos); + + for (const slug of chaptersOrVideos) { + if (!slugToVideo.has(slug)) continue; + + const video = slugToVideo.get(slug); + const parts = video.parts ?? [video]; + const isCanonicalTrack = video.canonicalTrack === trackSlug; + + parts.forEach((part, i) => { + if (!redirects[part.videoId] || isCanonicalTrack) { + const partAnchor = i > 0 ? `#part-${i + 1}` : ''; + redirects[part.videoId] = `/tracks/${trackSlug}/${slug}${partAnchor}`; + } + }); + } +} + +fs.writeFileSync( + 'netlify/edge-functions/yt/redirects.json', + JSON.stringify(redirects) +); + +console.log( + `${Object.keys(redirects).length} YouTube redirects were generated.` +); diff --git a/package.json b/package.json index 54ec4f8fb..4461be349 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "dev": "gatsby develop -H 0.0.0.0", "build": "gatsby build --verbose", - "build-ci": "node node-scripts/generate-challenges-redirects && npm run tags-transforms && npm run build", + "build-ci": "node node-scripts/generate-challenges-redirects && node node-scripts/generate-youtube-redirects && npm run tags-transforms && npm run build", "serve": "gatsby serve", "clean": "gatsby clean", "test": "jest",