Skip to content

Commit 1665179

Browse files
alanopsclaude
andcommitted
Add YouTube Thumbnail Resizer tool with Netlify deployment
- Built complete thumbnail resizing tool for YouTube content creators - Features drag-and-drop upload with multiple format support (JPG, PNG, WebP) - Automatic optimization to stay under 2MB file size limit - YouTube-optimized presets (1280x720 HD, 640x360 SD, custom sizes) - Quality control with adjustable compression (10-100%) - Batch processing for multiple images - Server-side processing using Sharp via Netlify Functions - Static export configuration for seamless Netlify deployment - Responsive design with Tailwind CSS matching existing UI - Complete deployment documentation and configuration Technical implementation: - Next.js API route converted to Netlify Function for server-side image processing - Client-side React interface with react-dropzone for modern UX - Sharp library for high-performance image manipulation - Static export optimization with proper build configuration - CORS configuration for cross-origin requests - TypeScript throughout for type safety 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4a0b391 commit 1665179

11 files changed

Lines changed: 11635 additions & 31 deletions

File tree

DEPLOY.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Deploy YouTube Thumbnail Resizer to Netlify
2+
3+
## 🚀 Quick Deploy
4+
5+
### Option 1: Drag & Drop (Easiest)
6+
1. Run build: `npm run build`
7+
2. Go to [netlify.com](https://netlify.com) and create account
8+
3. Drag the `out` folder to Netlify's deploy area
9+
4. Your site will be live at `https://random-name.netlify.app`
10+
11+
### Option 2: Git Deploy (Recommended)
12+
1. Push code to GitHub repository
13+
2. Connect repository to Netlify
14+
3. Netlify will auto-build using `netlify.toml` settings
15+
16+
### Option 3: Netlify CLI
17+
```bash
18+
# Install Netlify CLI
19+
npm install -g netlify-cli
20+
21+
# Login to Netlify
22+
netlify login
23+
24+
# Deploy
25+
netlify deploy --prod --dir=out
26+
```
27+
28+
## 📁 Project Structure for Netlify
29+
30+
```
31+
/workspaces/devopslearn/
32+
├── netlify.toml # Netlify configuration
33+
├── netlify/
34+
│ └── functions/
35+
│ └── thumbnail-resize.js # Server-side image processing
36+
├── out/ # Built static files (auto-generated)
37+
├── src/
38+
│ ├── pages/
39+
│ │ └── tools/
40+
│ │ └── thumbnail-resizer.tsx # Frontend UI
41+
│ └── utils/
42+
│ └── thumbnail-resizer.ts # Utility functions
43+
└── next.config.js # Next.js configuration for static export
44+
```
45+
46+
## ⚙️ Configuration Files
47+
48+
### netlify.toml
49+
- Builds static files to `out/` directory
50+
- Configures Netlify Functions
51+
- Sets up redirects and CORS headers
52+
53+
### next.config.js
54+
- Enables static export with `output: 'export'`
55+
- Disables image optimization for static hosting
56+
- Optimizes build for deployment
57+
58+
## 🔧 Environment Requirements
59+
60+
### Netlify Functions:
61+
- Node.js 18+
62+
- Sharp (for server-side image processing)
63+
- lambda-multipart-parser (for file uploads)
64+
65+
### Frontend:
66+
- Static HTML/CSS/JS (no server required)
67+
- React + Next.js (client-side only)
68+
- Tailwind CSS for styling
69+
70+
## 🌐 Live URL
71+
After deployment, your thumbnail resizer will be available at:
72+
- `https://your-site.netlify.app/tools/thumbnail-resizer`
73+
74+
## 🐞 Troubleshooting
75+
76+
### Build Issues:
77+
- Ensure all dependencies are installed: `npm install`
78+
- Check build logs in Netlify dashboard
79+
- Verify `out/` directory exists after build
80+
81+
### Function Issues:
82+
- Check function logs in Netlify dashboard
83+
- Verify Sharp binary compatibility with Netlify's environment
84+
- Test function endpoint: `/.netlify/functions/thumbnail-resize`
85+
86+
### CORS Issues:
87+
- CORS headers are configured in `netlify.toml`
88+
- Test with browser dev tools network tab
89+
90+
## 📊 Usage Stats
91+
Monitor usage in Netlify dashboard:
92+
- Function invocations
93+
- Bandwidth usage
94+
- Build minutes
95+
96+
Free tier includes:
97+
- 125K function requests/month
98+
- 100GB bandwidth/month
99+
- 300 build minutes/month

netlify.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[build]
2+
publish = "out"
3+
command = "npm run build"
4+
5+
[functions]
6+
directory = "netlify/functions"
7+
8+
[[redirects]]
9+
from = "/api/*"
10+
to = "/.netlify/functions/:splat"
11+
status = 200
12+
13+
[build.environment]
14+
NODE_VERSION = "18"
15+
NPM_VERSION = "9"
16+
17+
# Headers for CORS
18+
[[headers]]
19+
for = "/.netlify/functions/*"
20+
[headers.values]
21+
Access-Control-Allow-Origin = "*"
22+
Access-Control-Allow-Headers = "Content-Type"
23+
Access-Control-Allow-Methods = "GET, POST, PUT, DELETE, OPTIONS"
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
const sharp = require('sharp');
2+
const multipart = require('lambda-multipart-parser');
3+
4+
exports.handler = async (event, context) => {
5+
// Only allow POST requests
6+
if (event.httpMethod !== 'POST') {
7+
return {
8+
statusCode: 405,
9+
headers: {
10+
'Access-Control-Allow-Origin': '*',
11+
'Access-Control-Allow-Headers': 'Content-Type',
12+
'Access-Control-Allow-Methods': 'POST, OPTIONS'
13+
},
14+
body: JSON.stringify({ error: 'Method not allowed' })
15+
};
16+
}
17+
18+
// Handle CORS preflight
19+
if (event.httpMethod === 'OPTIONS') {
20+
return {
21+
statusCode: 200,
22+
headers: {
23+
'Access-Control-Allow-Origin': '*',
24+
'Access-Control-Allow-Headers': 'Content-Type',
25+
'Access-Control-Allow-Methods': 'POST, OPTIONS'
26+
},
27+
body: ''
28+
};
29+
}
30+
31+
try {
32+
// Parse multipart form data
33+
const result = await multipart.parse(event);
34+
35+
if (!result.files || !result.files.length) {
36+
return {
37+
statusCode: 400,
38+
headers: {
39+
'Access-Control-Allow-Origin': '*',
40+
'Content-Type': 'application/json'
41+
},
42+
body: JSON.stringify({ error: 'No image file provided' })
43+
};
44+
}
45+
46+
const file = result.files[0];
47+
const fileBuffer = Buffer.from(file.content, 'base64');
48+
49+
// Get resize options from form fields
50+
const width = parseInt(result.width || '1280');
51+
const height = parseInt(result.height || '720');
52+
const quality = parseInt(result.quality || '90');
53+
const format = result.format || 'jpeg';
54+
const maxSizeKB = parseInt(result.maxSizeKB || '2048');
55+
56+
// Get original image metadata
57+
const originalMetadata = await sharp(fileBuffer).metadata();
58+
const originalSizeKB = Math.round(fileBuffer.length / 1024);
59+
60+
// Resize the image
61+
let resizedBuffer = await sharp(fileBuffer)
62+
.resize(width, height, {
63+
fit: 'cover',
64+
position: 'centre'
65+
})
66+
.toFormat(format, { quality })
67+
.toBuffer();
68+
69+
// If the file is still too large, reduce quality iteratively
70+
let currentQuality = quality;
71+
while (resizedBuffer.length > maxSizeKB * 1024 && currentQuality > 10) {
72+
currentQuality -= 10;
73+
resizedBuffer = await sharp(fileBuffer)
74+
.resize(width, height, {
75+
fit: 'cover',
76+
position: 'centre'
77+
})
78+
.toFormat(format, { quality: currentQuality })
79+
.toBuffer();
80+
}
81+
82+
const resizedSizeKB = Math.round(resizedBuffer.length / 1024);
83+
84+
// Return the resized image
85+
return {
86+
statusCode: 200,
87+
headers: {
88+
'Access-Control-Allow-Origin': '*',
89+
'Content-Type': `image/${format}`,
90+
'Content-Disposition': `attachment; filename="thumbnail.${format}"`,
91+
'X-Original-Size': originalSizeKB.toString(),
92+
'X-Resized-Size': resizedSizeKB.toString(),
93+
'X-Original-Dimensions': `${originalMetadata.width}x${originalMetadata.height}`,
94+
'X-Resized-Dimensions': `${width}x${height}`
95+
},
96+
body: resizedBuffer.toString('base64'),
97+
isBase64Encoded: true
98+
};
99+
} catch (error) {
100+
console.error('Error processing image:', error);
101+
return {
102+
statusCode: 500,
103+
headers: {
104+
'Access-Control-Allow-Origin': '*',
105+
'Content-Type': 'application/json'
106+
},
107+
body: JSON.stringify({ error: 'Failed to process image' })
108+
};
109+
}
110+
};

next.config.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
/** @type {import('next').NextConfig} */
22
const nextConfig = {
33
reactStrictMode: true,
4-
swcMinify: true,
5-
async headers() {
6-
return [
7-
{
8-
source: '/:path*',
9-
headers: [
10-
{
11-
key: 'X-Frame-Options',
12-
value: 'DENY',
13-
},
14-
],
15-
},
16-
]
4+
trailingSlash: true,
5+
output: 'export',
6+
images: {
7+
unoptimized: true
8+
},
9+
// Disable API routes in static export
10+
experimental: {
11+
// This will be handled by Netlify Functions
12+
outputFileTracingExcludes: {
13+
'*': [
14+
'node_modules/@swc/core-linux-x64-gnu',
15+
'node_modules/@swc/core-linux-x64-musl',
16+
'node_modules/@esbuild/linux-x64',
17+
],
18+
},
1719
},
1820
}
1921

0 commit comments

Comments
 (0)