Skip to content

Commit 7f7f9c4

Browse files
committed
add /get-thumbnail utility
1 parent ae387c7 commit 7f7f9c4

File tree

1 file changed

+184
-0
lines changed

1 file changed

+184
-0
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
---
2+
/**
3+
* YouTube Thumbnail Extractor (Using Cursor)
4+
*
5+
* PURPOSE:
6+
* - Single-file solution that serves both UI and API functionality
7+
* - Extracts YouTube video IDs from various URL formats and redirects to max resolution thumbnails
8+
*
9+
* URL STRUCTURE:
10+
* - UI: https://chan.dev/get-thumbnail (shows form)
11+
* - API: https://chan.dev/get-thumbnail?url=YOUTUBE_URL (redirects to thumbnail)
12+
*
13+
* SUPPORTED YOUTUBE URL FORMATS:
14+
* - https://www.youtube.com/watch?v=VIDEO_ID
15+
* - https://youtu.be/VIDEO_ID
16+
* - https://www.youtube.com/embed/VIDEO_ID
17+
*
18+
* KEY IMPLEMENTATION DETAILS:
19+
* - Uses `export const prerender = false` to enable server-side rendering
20+
* - Single regex pattern handles all YouTube URL formats
21+
* - Redirects to https://img.youtube.com/vi/VIDEO_ID/maxresdefault.jpg
22+
* - Includes 1-hour cache headers for performance
23+
*
24+
* DISCUSSION HISTORY:
25+
* - Originally implemented as separate API route + UI page
26+
* - Converted to single-file solution per user preference
27+
* - Simplified regex patterns from 3 separate patterns to 1 consolidated pattern
28+
* - Removed unnecessary variables and TypeScript casting
29+
* - Reduced code by ~26% while maintaining identical functionality
30+
*
31+
* IMPORTANT NOTES FOR EDITING:
32+
* - Frontmatter logic runs before HTML rendering - API responses must be returned early
33+
* - The `hasUrlParam` check determines whether to show UI or act as API
34+
* - Error responses use JSON format for API consistency
35+
* - Form uses optional chaining (?.) to handle potential null elements
36+
* - TypeScript casting needed for input element value access
37+
* - Cache headers help with performance but can be adjusted as needed
38+
*
39+
* DEPLOYMENT:
40+
* - Works with Cloudflare Pages (hybrid output mode)
41+
* - No additional dependencies required
42+
* - Ready for production deployment
43+
*/
44+
45+
import Layout from '#layouts/Layout.astro'
46+
47+
export const prerender = false
48+
49+
// Handle API requests
50+
const url = Astro.url
51+
const hasUrlParam = url.searchParams.has('url')
52+
53+
if (hasUrlParam) {
54+
const videoUrl = url.searchParams.get('url')
55+
56+
if (!videoUrl) {
57+
return new Response(
58+
JSON.stringify({
59+
error:
60+
'Missing video URL parameter. Use ?url=https://www.youtube.com/watch?v=VIDEO_ID',
61+
}),
62+
{
63+
status: 400,
64+
headers: {'Content-Type': 'application/json'},
65+
}
66+
)
67+
}
68+
69+
const videoId = extractYouTubeVideoId(videoUrl)
70+
71+
if (!videoId) {
72+
return new Response(
73+
JSON.stringify({
74+
error:
75+
'Invalid YouTube URL. Could not extract video ID.',
76+
}),
77+
{
78+
status: 400,
79+
headers: {'Content-Type': 'application/json'},
80+
}
81+
)
82+
}
83+
84+
return new Response(null, {
85+
status: 302,
86+
headers: {
87+
'Location': `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
88+
'Cache-Control': 'public, max-age=3600',
89+
},
90+
})
91+
}
92+
93+
function extractYouTubeVideoId(url: string): string | null {
94+
// Single regex pattern that handles all YouTube URL formats
95+
const match = url.match(
96+
/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^&\n?#]+)/
97+
)
98+
return match ? match[1] : null
99+
}
100+
---
101+
102+
<Layout>
103+
<div class="max-w-2xl mx-auto p-6">
104+
<h1 class="text-3xl font-bold mb-6">
105+
YouTube Thumbnail Extractor
106+
</h1>
107+
108+
<div class="bg-gray-50 p-6 rounded-lg mb-6">
109+
<h2 class="text-xl font-semibold mb-4">How to use:</h2>
110+
<p class="mb-2">
111+
Add your YouTube URL as a query parameter:
112+
</p>
113+
<code class="bg-gray-200 p-2 rounded block mb-4">
114+
https://chan.dev/get-thumbnail?url=https://www.youtube.com/watch?v=SPwPpsXpZfg
115+
</code>
116+
<p class="text-sm text-gray-600">
117+
This will redirect you to the maximum resolution
118+
thumbnail for that video.
119+
</p>
120+
</div>
121+
122+
<div class="bg-blue-50 p-6 rounded-lg">
123+
<h2 class="text-xl font-semibold mb-4">Test it:</h2>
124+
<form id="thumbnailForm" class="space-y-4">
125+
<div>
126+
<label
127+
for="videoUrl"
128+
class="block text-sm font-medium mb-2"
129+
>
130+
YouTube URL:
131+
</label>
132+
<input
133+
type="url"
134+
id="videoUrl"
135+
name="videoUrl"
136+
placeholder="https://www.youtube.com/watch?v=SPwPpsXpZfg"
137+
class="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
138+
required
139+
/>
140+
</div>
141+
<button
142+
type="submit"
143+
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
144+
>
145+
Get Thumbnail
146+
</button>
147+
</form>
148+
</div>
149+
150+
<div class="mt-8">
151+
<h2 class="text-xl font-semibold mb-4">
152+
Supported URL formats:
153+
</h2>
154+
<ul class="list-disc list-inside space-y-2 text-sm">
155+
<li>
156+
<code>https://www.youtube.com/watch?v=VIDEO_ID</code>
157+
</li>
158+
<li><code>https://youtu.be/VIDEO_ID</code></li>
159+
<li>
160+
<code>https://www.youtube.com/embed/VIDEO_ID</code>
161+
</li>
162+
</ul>
163+
</div>
164+
</div>
165+
166+
<script>
167+
document
168+
.getElementById('thumbnailForm')
169+
?.addEventListener('submit', function (e) {
170+
e.preventDefault()
171+
const videoUrl = (
172+
document.getElementById(
173+
'videoUrl'
174+
) as HTMLInputElement
175+
)?.value
176+
if (videoUrl) {
177+
window.open(
178+
`/get-thumbnail?url=${encodeURIComponent(videoUrl)}`,
179+
'_blank'
180+
)
181+
}
182+
})
183+
</script>
184+
</Layout>

0 commit comments

Comments
 (0)