Full Stack Personal Blog 10: CDN Storage
cz2025.06.03 15:04

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.

1. Register an AWS account, create an S3 Bucket, and obtain the following credentials to paste into your .env file

APP_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'

2. S3 Access Permissions and CORS Settings

  • 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": []
      }
    ]
    

3. Upload Static Assets to S3 Bucket After Next.js Build

// 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)
  })

4. Adjust the assetPrefix for Static Assets in Production

// next.config.ts
const nextConfig: NextConfig = {
  /* config options here */
  assetPrefix: __IS_PROD__ ? NEXT_PUBLIC_AWS_S3_ASEETSPREFIX : undefined
}

Comments