Why Host Your Website Images on AWS S3?
If your website loads slowly, the culprit is almost always images. Large, unoptimized images served from your web server eat bandwidth, slow page loads, and crush your Core Web Vitals — all of which hurt your Google rankings.
Amazon S3 (Simple Storage Service) solves this by offloading your images to a globally distributed, infinitely scalable object store. Combined with CloudFront (AWS's CDN) and a custom CNAME, your images load faster, cost less, and — critically — boost your SEO instead of hurting it.
This guide walks you through the entire process: creating your S3 bucket, configuring CloudFront with a custom domain, setting it up in WordPress and React, and the SEO best practices that tie it all together.
📋 What You'll Need
- An AWS account (free tier works for getting started)
- A domain you control (e.g.,
yourdomain.com) - Access to your DNS provider (GoDaddy, Cloudflare, Route 53, etc.)
- An SSL certificate (free via AWS Certificate Manager)
Step 1: Create Your S3 Bucket
- Sign in to the AWS Console and navigate to S3.
- Click "Create bucket" — name it something descriptive like
media.yourdomain.com. - Region: Choose the region closest to your audience (e.g.,
us-east-1for US visitors). - Block Public Access: Leave this ON — we'll serve files through CloudFront, not directly from S3.
- Versioning: Enable it. This protects against accidental overwrites.
- Click Create bucket.
💡 Pro Tip: Organize images into folders like /blog/, /products/, /team/. This makes management easier and lets you set different cache policies per folder.
Upload Your First Images
Click into your bucket, then Upload → Add files. Drag your images in. For each image, set the Content-Type metadata correctly (image/webp, image/jpeg, etc.) — this ensures browsers render them correctly.
Step 2: Set Up CloudFront (CDN)
Serving images directly from an S3 URL like https://my-bucket.s3.amazonaws.com/hero.jpg is a bad idea for SEO. Search engines associate link equity with domains — and you want that equity flowing to your domain, not Amazon's.
CloudFront sits in front of S3 and serves your images from edge locations worldwide. More importantly, it lets you attach your own custom domain.
Create a CloudFront Distribution
- Go to CloudFront in the AWS Console → Create Distribution.
- Origin domain: Select your S3 bucket from the dropdown.
- Origin access: Choose "Origin access control settings (recommended)" — this creates a secure connection between CloudFront and S3 without making S3 public.
- Viewer protocol policy: Set to "Redirect HTTP to HTTPS".
- Cache policy: Use CachingOptimized (or create a custom policy with a long TTL for images).
- Alternate domain name (CNAME): Enter
media.yourdomain.com. - Custom SSL certificate: Request a free certificate from AWS Certificate Manager (ACM). Important: ACM certificates for CloudFront must be in
us-east-1. - Click Create distribution.
Update the S3 Bucket Policy
After creating the distribution, AWS will prompt you to update your S3 bucket policy. Copy the policy it provides and paste it into your bucket's Permissions → Bucket policy. This grants CloudFront (and only CloudFront) permission to read your objects.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::media.yourdomain.com/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DIST_ID"
}
}
}]
}
Step 3: Configure Your CNAME (The SEO Secret)
This is the step most tutorials skip — and it's the most important one for SEO.
By default, CloudFront gives you a URL like d1234abcdef8.cloudfront.net. If you use that in your <img> tags, every image link on your site points to Amazon's domain — not yours. That's wasted link equity.
Add a CNAME Record in DNS
Go to your DNS provider and create a CNAME record:
| Type | Name | Value | TTL |
|---|---|---|---|
| CNAME | media |
d1234abcdef8.cloudfront.net |
3600 |
Now your images are served from https://media.yourdomain.com/blog/hero.jpg — a URL that lives on your domain and contributes to your site's authority.
Why This Matters for SEO
- Domain authority stays with you — Image links from external sites point to your subdomain, not Amazon's.
- Google Image Search traffic — Images indexed under
media.yourdomain.comdrive traffic to your site, not AWS. - Consistent branding — Users who inspect image URLs see your domain, building trust.
- Faster page loads — CloudFront's global CDN means images load from the nearest edge location, improving Core Web Vitals (LCP, CLS).
- No mixed signals — Search engines won't associate your site with dozens of third-party domains.
If you're already struggling with SSL configuration, our SSL Certificate Setup Guide covers the fundamentals of TLS certificates for small businesses.
Step 4: WordPress Integration
WordPress makes S3 integration straightforward with the right plugin.
Option A: WP Offload Media (Recommended)
- Install and activate WP Offload Media Lite from the WordPress plugin directory.
- Go to Settings → Offload Media.
- Choose Amazon S3 as your storage provider.
- Enter your AWS access key and secret key (create an IAM user with S3 and CloudFront permissions).
- Select your bucket (
media.yourdomain.com). - Under Delivery, choose "CloudFront or other CDN" and enter
media.yourdomain.comas the custom domain. - Enable "Remove Files From Server" to free up hosting storage.
Now every image you upload through WordPress will automatically go to S3 and be served via your CloudFront CNAME. Existing images can be offloaded in bulk.
Option B: wp-config.php Constants
If you prefer not to store AWS credentials in the plugin UI, add them to wp-config.php:
define( 'AS3CF_SETTINGS', serialize( array(
'provider' => 'aws',
'access-key-id' => 'YOUR_ACCESS_KEY',
'secret-access-key' => 'YOUR_SECRET_KEY',
) ) );
⚠️ Security Note: Never commit AWS credentials to version control. Use environment variables or AWS IAM roles if your server supports them. For WordPress security best practices, see our WooCommerce Security Checklist.
Step 5: React / Vite / Next.js Integration
For modern JavaScript frameworks, you'll reference your S3 images via the CloudFront CNAME directly.
Environment Variable Setup
Store your CDN URL in an environment variable so it's easy to change across environments:
# .env
VITE_CDN_URL=https://media.yourdomain.com
# or for Next.js
NEXT_PUBLIC_CDN_URL=https://media.yourdomain.com
Using It in Components
// React / Vite
const CDN = import.meta.env.VITE_CDN_URL;
function HeroImage() {
return (
<img
src={`${CDN}/blog/hero-banner.webp`}
alt="Descriptive alt text for SEO"
width={1200}
height={630}
loading="lazy"
/>
);
}
Automating Uploads with the AWS SDK
For build-time or CMS-driven uploads, use the AWS SDK:
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" });
async function uploadImage(file: Buffer, key: string) {
await s3.send(new PutObjectCommand({
Bucket: "media.yourdomain.com",
Key: key,
Body: file,
ContentType: "image/webp",
CacheControl: "public, max-age=31536000, immutable",
}));
return `https://media.yourdomain.com/${key}`;
}
The CacheControl header tells CloudFront (and browsers) to cache the image for one year — perfect for immutable assets with unique filenames. If you're deploying this via a pipeline, see our CI/CD Pipeline Guide for automating the process.
Step 6: Image Optimization Best Practices
Hosting on S3 doesn't automatically mean your images are optimized. Follow these rules:
Format Selection
- WebP — Default choice for photos and complex images. 25-35% smaller than JPEG at equal quality.
- AVIF — Even smaller than WebP, but slower to encode. Great for hero images and high-value assets.
- SVG — Use for logos, icons, and illustrations. Infinitely scalable, tiny file size.
- PNG — Only for images that need transparency and can't use WebP.
Sizing & Responsive Images
<picture>
<source
srcset="https://media.yourdomain.com/hero-400.webp 400w,
https://media.yourdomain.com/hero-800.webp 800w,
https://media.yourdomain.com/hero-1200.webp 1200w"
type="image/webp"
sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px"
/>
<img
src="https://media.yourdomain.com/hero-800.jpg"
alt="Hero image for website"
width="1200"
height="630"
loading="lazy"
decoding="async"
/>
</picture>
Upload multiple sizes of each image to S3. This lets the browser download only the size it needs, dramatically improving mobile performance.
SEO Image Tags Checklist
- ✅ Alt text — Descriptive, keyword-rich (but natural). "Blue 2026 Toyota Camry parked in downtown San Antonio" > "car".
- ✅ Width & height attributes — Prevents Cumulative Layout Shift (CLS).
- ✅ Lazy loading —
loading="lazy"for below-the-fold images. - ✅ Descriptive filenames —
san-antonio-office-team.webp>IMG_4523.jpg. - ✅ Structured data — Add
ImageObjectschema for important images.
Step 7: Cache Invalidation & Versioning
When you update an image, CloudFront may still serve the old cached version. You have two strategies:
Strategy A: Cache Busting with File Hashes (Recommended)
Append a hash to filenames: hero-a3f8b2c.webp. When the image changes, the hash changes, and CloudFront treats it as a new file. This is what Vite and Webpack do automatically for bundled assets.
Strategy B: CloudFront Invalidation
For images with fixed names (like logo.png), create an invalidation in the CloudFront console:
# AWS CLI
aws cloudfront create-invalidation \
--distribution-id YOUR_DIST_ID \
--paths "/logo.png" "/team/*"
CloudFront gives you 1,000 free invalidation paths per month. After that, it's $0.005 per path.
Cost Breakdown: What Will This Cost?
For a typical small-to-medium business website:
| Service | Usage | Monthly Cost |
|---|---|---|
| S3 Storage | 10 GB of images | ~$0.23 |
| CloudFront Data Transfer | 50 GB/month | ~$4.25 |
| CloudFront Requests | 500K requests | ~$0.50 |
| ACM Certificate | 1 certificate | Free |
| Total | ~$5/month |
Compare that to the cost of lost customers from a slow website. For most businesses, S3 + CloudFront pays for itself many times over.
Frequently Asked Questions
Can I use S3 without CloudFront?
Technically yes, but it's not recommended. Without CloudFront you lose CDN caching, custom domain support, and HTTPS — all of which hurt performance and SEO.
Will this work with any CMS, not just WordPress?
Absolutely. Shopify, Drupal, Joomla, Ghost, and static site generators all work. The S3 + CloudFront + CNAME pattern is universal — only the integration method changes.
Does using a subdomain (media.domain.com) hurt SEO?
No. Google treats subdomains as part of the same property in Search Console (when configured), and the subdomain inherits your domain's authority. It's vastly better than using an amazonaws.com or cloudfront.net URL.
What about image sitemaps?
Yes — add your CDN-hosted images to your XML sitemap using the <image:image> tag. Google recommends this for important images you want indexed.
📖 Related Guides
SSL Certificate Setup Guide for Small Businesses — Get HTTPS working properly on your site.
What Is a CI/CD Pipeline? — Automate your image uploads and deployments.
WooCommerce Security Checklist 2026 — Security best practices for WordPress stores.
GitHub + VPS Hosting Setup — Deploy and manage your server-side infrastructure.
