Choose AWS S3, 5GB storage, free for 12 months. The main reason is that Vercel hosts the project, and free users get 100GB of traffic per month. If you exceed this and don't pay, your account will be disabled, so you must add a CDN.
.env
fileAPP_AWS_ACCESS_KEY = 'xxx'
APP_AWS_SECRET_KEY = 'xxx+xxx'
APP_AWS_REGION = 'ap-southeast-2'
AWS_S3_BUCKET_NAME_ASSETS = 'thisiscz-assets'
NEXT_PUBLIC_AWS_S3_BUCKET_NAME_ASSETS = 'thisiscz-assets'
NEXT_PUBLIC_AWS_S3_ASEETSPREFIX = 'https://thisiscz-assets.s3.ap-southeast-2.amazonaws.com'
Allow all public access
Bucket policy
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "AllowPublicRead",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::thisiscz-assets/*"
}
]
}
CORS configuration
Allow your own domain to access
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"http://localhost:3000",
"https://thisiscz.vercel.app"
],
"ExposeHeaders": []
}
]
// package.json
"build": "next build && node uploadToS3.js",
You can upload both the public
directory and the built static assets from .next/static/
.
// upload-to-s3.js
const AWS = require('aws-sdk')
const fs = require('fs')
const path = require('path')
require('dotenv').config()
// Function to get Content-Type
function getContentType(filePath) {
const ext = path.extname(filePath).toLowerCase()
const contentTypes = {
'.css': 'text/css',
'.js': 'application/javascript',
'.html': 'text/html',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'font/otf',
}
return contentTypes[ext] || 'application/octet-stream'
}
const s3 = new AWS.S3({
accessKeyId: process.env.APP_AWS_ACCESS_KEY,
secretAccessKey: process.env.APP_AWS_SECRET_KEY,
region: process.env.APP_AWS_REGION,
})
async function uploadDir(dirPath, s3Path = '') {
const files = fs.readdirSync(dirPath)
for (const file of files) {
const filePath = path.join(dirPath, file)
const s3Key = path.join(s3Path, path.relative(process.cwd(), filePath))
const fileStat = fs.statSync(filePath)
if (fileStat.isDirectory()) {
await uploadDir(filePath, s3Path)
} else {
const fileContent = fs.readFileSync(filePath)
const params = {
Bucket: process.env.AWS_S3_BUCKET_NAME_ASSETS,
Key: s3Key.replace('.next', '_next'),
Body: fileContent,
ContentType: getContentType(filePath), // Add ContentType
}
try {
await s3.upload(params).promise()
console.log(
`Uploaded ${filePath} to s3://${params.Bucket}/${params.Key} with Content-Type: ${params.ContentType}`,
)
} catch (err) {
console.error(`Error uploading ${filePath}:`, err)
}
}
}
}
const assetsDir = path.join(process.cwd(), '.next/static/')
const publicDir = path.join(process.cwd(), 'public/')
uploadDir(assetsDir)
.then(() => {
console.log('Upload assets complete!')
})
.catch((err) => {
console.error('Upload assets failed:', err)
})
uploadDir(publicDir)
.then(() => {
console.log('Upload public files complete!')
})
.catch((err) => {
console.error('Upload public files failed:', err)
})
assetPrefix
for Static Assets in Production// next.config.ts
const nextConfig: NextConfig = {
/* config options here */
assetPrefix: __IS_PROD__ ? NEXT_PUBLIC_AWS_S3_ASEETSPREFIX : undefined
}