CDN Setup for CMS Sites: Speed Up Static and Dynamic Content
Cloudflare walkthrough, cache rules, and before/after metrics.
A site that loads in 4 seconds loses roughly half its visitors before they read a single word. Google's own data puts the bounce-rate jump at 90% when load time goes from 1 to 5 seconds — and that was measured before Core Web Vitals became a ranking factor. Today, LCP (Largest Contentful Paint) under 2.5 seconds isn't just a nice UX goal; it's a threshold that determines whether you compete on page one or page three.
A CDN for your CMS won't fix bad code or a bloated database. But for the static and semi-static content that most CMS sites serve — images, CSS, JavaScript, HTML, font files — a CDN cuts delivery time by 50-80% without touching a line of PHP.
TL;DR: A CDN puts copies of your static files on servers closer to your visitors, so they don't wait for a round trip to your VPS in Frankfurt when they're in Singapore. For self-hosted CMS sites, Cloudflare's free tier is the right first move: point your DNS, enable proxy, set a few cache rules, and your LCP drops without any code changes. Image optimization and cache bypass rules for your admin panel are the two things you configure next.
Why Self-Hosted CMS Sites Especially Need a CDN
When you run a SaaS CMS like Contentful or Webflow, the vendor handles delivery infrastructure. You pay a monthly fee partly to get their edge network. When you self-host — on a $5 VPS, a dedicated server, or even shared hosting — your content is served from one place. Every request, from every visitor, anywhere on Earth, hits that one box.
That's fine for small traffic. The problem appears at scale, or when your audience is geographically spread:
- A 200ms round-trip to your server becomes 400ms for a visitor 8,000 miles away, before the page even starts loading
- Static assets (images, fonts, JS bundles) are re-downloaded from the same origin on every page load unless cached at the browser
- A traffic spike — from a viral post, a product launch, a newsletter drop — can push your VPS's CPU or bandwidth past its limit
A CDN solves the first two by caching your content at edge nodes. The third one (traffic spikes) it also absorbs, because the CDN handles most requests and only fetches from your origin when the cache is cold.
For a self-hosted CMS that you're setting up for real traffic — whether following the complete self-hosted CMS guide or building on a VPS — a CDN is infrastructure, not a luxury.
CDN Options for Self-Hosted CMS Sites
Not all CDNs are built the same, and the choice depends on your traffic, budget, and how much you want to configure.
Cloudflare is the default recommendation for self-hosted CMS sites. The free tier covers unlimited CDN bandwidth, DDoS protection, automatic HTTPS, and basic WAF (Web Application Firewall). You just move your DNS to Cloudflare and proxy your A record. It takes 15 minutes and zero server changes.
BunnyCDN is worth adding when you start serving a lot of images or video. At fractions of a cent per gigabyte and a global edge network (108 PoPs as of 2025), it's cheaper than Cloudflare's paid tiers for pure asset delivery. Many teams run Cloudflare for the main site and BunnyCDN specifically for their image CDN.
Fastly and CloudFront are enterprise tools. Real-time cache purging, custom VCL edge logic, Lambda@Edge — they're powerful, but they require engineering time to set up correctly. Unless you're running a high-traffic media site or already inside AWS, they're overkill for a CMS blog or marketing site.
What to Serve Through a CDN (and What to Keep on Your Server)
Not everything should go through a CDN cache. Getting this wrong means visitors see stale admin pages or cached form submissions.
CDN — yes:
- Images (hero images, post thumbnails, inline media)
- CSS and JavaScript bundles (your CMS frontend assets)
- Font files
- Static HTML pages that don't change per user
- Sitemap.xml and robots.txt
- Open Graph images, favicons
CDN — no or bypass:
- Admin panel (
/admin,/cms) — always bypass. Caching admin pages breaks logins, CSRF tokens, and gives unauthenticated users cached admin HTML - Checkout and payment pages — any flow with session state
- API endpoints that return personalized or real-time data
- POST, PUT, DELETE requests — CDNs should never cache write operations
- Authenticated pages (user dashboards, subscriber-only content)
- WordPress-style PHP pages with dynamic per-session content
The rule is simple: if the response is the same for every visitor and doesn't contain session data → cache it. If it varies by user, session, or real-time state → bypass.
For most CMS blog sites, 85%+ of traffic goes to public posts and pages. All of that can be cached. Your security setup already handles the admin panel at the server level — the CDN should honor that by skipping it entirely.
How to Set Up Cloudflare for a Self-Hosted CMS on a VPS
This works for any self-hosted CMS running on a VPS — whether you set it up following a step-by-step guide or migrated from a managed platform.
-
Create a Cloudflare account at cloudflare.com and add your domain (e.g.,
yoursite.com). -
Update your nameservers. Cloudflare gives you two nameservers (e.g.,
maya.ns.cloudflare.com). Go to your domain registrar and replace the existing nameservers with these. DNS propagation takes a few minutes to a few hours. -
Enable proxy on your A record. In the Cloudflare DNS panel, find your A record pointing to your VPS IP. Click the orange cloud icon — it should show "Proxied." This tells Cloudflare to intercept requests and serve from its edge.
-
Set SSL to "Full (Strict)." Go to SSL/TLS → Overview and select Full (Strict). This means traffic from Cloudflare to your origin is also encrypted. You need a valid SSL cert on your server (Let's Encrypt works). "Flexible" mode (encrypts only Cloudflare-to-visitor) is a security risk — don't use it.
-
Enable "Always Use HTTPS." Under SSL/TLS → Edge Certificates, toggle "Always Use HTTPS" on. This redirects all HTTP traffic to HTTPS at the CDN edge before it hits your server.
-
Enable Auto Minify. Under Speed → Optimization, enable HTML, CSS, and JS minification. This reduces file size without you touching your CMS files.
-
Enable Brotli compression. Same section — enable Brotli. For text assets (HTML, CSS, JS), this typically cuts 15-25% off transfer size versus gzip.
-
Set your caching level. Under Caching → Configuration, set Caching Level to "Standard." This caches files based on their extension and query strings.
-
Create a Page Rule (or Cache Rule) to bypass admin. Go to Rules → Cache Rules. Create a rule: if the URL path starts with
/adminOR/cms→ Cache status: Bypass. This is critical — without it, Cloudflare may serve cached admin HTML. -
Purge the cache after publishing content. Each time you publish or update a post, either purge everything (fast but nuclear) or purge specific URLs. Most self-hosted CMS platforms can trigger a Cloudflare cache purge via API. Add your Cloudflare API token to your CMS settings if available.
-
Verify with a speed test. Open PageSpeed Insights (pagespeed.web.dev) and test your homepage. Check that the
cf-cache-statusresponse header saysHITon the second request (use browser dev tools, Network tab).
Cache Rules for a Blog CMS
The default Cloudflare caching works for most assets. For a blog-heavy CMS, you want to be more specific.
Cache these aggressively (set long TTLs):
- Images in
/storage/,/uploads/,/media/— these almost never change; a 30-day TTL is safe - Versioned JS/CSS assets (files with a hash in the name like
app.abc123.js) — these can be cached forever; when you deploy new code, the filename changes - Fonts (
*.woff2,*.woff) — set a 1-year TTL
Cache with shorter TTLs or conditional logic:
- Blog post HTML — 4-24 hours is fine for most sites. When you publish a new post, purge the blog index page
- Homepage and category pages — these change when new content is published; 1-4 hours is reasonable
- RSS feeds — cache for 30-60 minutes
Always bypass:
- Any URL matching
/admin*,/user*,/account*,/checkout* - Requests with
Authorizationheaders or session cookies for logged-in areas - All POST, PUT, PATCH, DELETE methods — Cloudflare skips caching these by default, but worth confirming in your logs
- Pages behind authentication (check your CMS's login-check routes)
Here's a quick Cloudflare cache rule pattern in plain English:
If URL path matches /admin/* OR /user/* OR /account/*
→ Cache: Bypass
If URL path matches /storage/media/* OR /uploads/*
→ Cache: Cache everything, Edge TTL: 30 days, Browser TTL: 7 days
Everything else
→ Cache: Standard (follow cache-control headers from origin)
Your CMS should send correct Cache-Control headers from the server. If it doesn't, Cloudflare uses its default heuristics. Check the cf-cache-status header in your browser's Network tab — HIT means it's serving from cache, MISS means it went to origin, BYPASS means the rule told it to skip.
Image CDN: WebP, Lazy Loading, and srcset
Images are typically 60-80% of a page's total byte size. For a CMS site with post thumbnails, hero images, and inline media, image optimization has more impact than any other single change.
Serve WebP automatically. Cloudflare's free plan converts images to WebP for browsers that support it (which is now basically all of them). Under Speed → Optimization → Image Optimization, enable "Polish" on the free tier (available as "Lossless" or "Lossy"). This reduces image size 25-50% without resolution loss.
If you're running BunnyCDN or a dedicated image CDN, look for their image processing feature (BunnyCDN calls it "BunnyOptimizer"). You can pass parameters like ?width=800&format=webp in the URL and the CDN resizes and converts on the fly.
Use srcset for responsive images. Don't serve a 2400px hero image to mobile users. Your CMS should generate multiple image sizes and let the browser pick:
<img
src="/storage/posts/hero.webp"
srcset="/storage/posts/hero-480.webp 480w, /storage/posts/hero-800.webp 800w, /storage/posts/hero-1200.webp 1200w"
sizes="(max-width: 600px) 480px, (max-width: 1000px) 800px, 1200px"
alt="Cloudflare CDN setup diagram showing edge nodes and origin server"
loading="lazy"
/>
Don't lazy-load the hero image. The hero is the Largest Contentful Paint element. Lazy loading it forces the browser to wait, pushing your LCP up. Use loading="eager" or just omit the attribute on your above-the-fold image. Add fetchpriority="high" to tell the browser to prioritize it.
Lazy-load everything below the fold. Every image further down the page should have loading="lazy". Modern browsers handle this natively.
Use descriptive filenames and alt text. Filenames matter for Google Image Search. cms-hosting-vps-server-rack.webp is better than IMG_4893.jpg. Alt text should describe what's in the image with relevant context — both for accessibility and for image ranking.
Measuring the Improvement
Before you make any CDN changes, capture a baseline. You can't call it an improvement if you don't know where you started.
Tools to use:
- PageSpeed Insights (pagespeed.web.dev) — Google's tool measuring Core Web Vitals. Tests both mobile and desktop. Shows LCP, CLS, INP scores from real-world Chrome data plus lab tests. Free.
- GTmetrix (gtmetrix.com) — Waterfalls, timing breakdown, asset sizes. Good for seeing which files are slowest. Free tier lets you test from multiple locations.
- WebPageTest (webpagetest.org) — Most detailed. Shows first-byte time, content loading waterfall, repeat-view (cached) vs first-view. Lets you test from specific cities.
What to measure before and after:
- LCP — should drop when hero images are served from CDN edge
- TTFB (Time to First Byte) — this one may not improve if your HTML is generated dynamically; CDN helps more with assets than with server-rendered HTML
- Total transfer size — should drop with WebP conversion and minification
- Cache hit rate — check your Cloudflare Analytics panel; aim for 70%+ cache hit ratio on a blog CMS
A real example of what to expect:
A self-hosted CMS blog running on a single $20/month VPS in Europe, tested from the US East Coast:
| Metric | Before CDN | After Cloudflare Free | Improvement |
|---|---|---|---|
| LCP | 4.1s | 1.8s | 56% faster |
| Total page size | 2.4MB | 1.1MB | 54% smaller |
| TTFB | 380ms | 190ms | 50% faster |
| Cache hit rate | 0% | 74% | — |
These numbers are representative of typical results. Your mileage will vary based on server location, existing caching, and image optimization state.
For more on Core Web Vitals specifically and what metrics matter for your CMS's search ranking, see core web vitals for CMS sites — coming soon.
Frequently Asked Questions
Does a CDN help with WordPress, or only modern CMS platforms?
A CDN helps any website that serves static assets — WordPress included. Cloudflare works the same way regardless of what CMS you run underneath. The main difference is that WordPress generates most pages dynamically (PHP per request), so the CDN cache hit rate for HTML is lower unless you add a caching plugin like WP Rocket or W3 Total Cache. Modern self-hosted CMS platforms that generate static HTML at publish time benefit more from CDN caching.
Will a CDN break my admin panel login?
Not if you set the bypass rule correctly. The bypass rule for /admin* tells Cloudflare to send requests directly to your origin without caching. Login, CSRF tokens, and session cookies all work normally. The problem only appears if you skip the bypass rule and let Cloudflare cache admin HTML — then users can get stale login pages. Set the rule first, test login, then proceed.
How much does Cloudflare's free tier actually cover?
Cloudflare Free includes: unlimited CDN bandwidth, automatic HTTPS, DDoS protection, HTTP/3 and QUIC, basic WAF (5 rules), image optimization (Polish — WebP conversion), and global distribution across Cloudflare's 300+ PoPs. The paid tiers ($20/month Pro, $200/month Business) add: more WAF rules, image resizing on the fly, priority support, and analytics with longer history. For most self-hosted CMS sites under 1 million monthly visitors, the free tier is enough.
Do I need a CDN if I'm already hosting on a fast VPS or dedicated server?
Yes, but for a different reason. A fast VPS reduces your server processing time, but it doesn't reduce the network latency for visitors far from your server. A visitor in Tokyo hitting your Frankfurt VPS still has 250ms of round-trip network time before a byte is transferred. A CDN solves that by serving from an edge node in Tokyo. A fast server plus a CDN is always better than either one alone.
Where to Start
If you're running a self-hosted CMS and haven't set up a CDN yet, start with Cloudflare Free today. It takes 15 minutes, it's free, and it's the single highest-ROI performance change you can make without touching your code.
The order of operations:
- Move DNS to Cloudflare, enable proxy
- Set SSL to Full (Strict)
- Add the admin bypass cache rule
- Enable Auto Minify and Polish (WebP)
- Measure with PageSpeed Insights before and after
- Tune TTLs on image and asset paths
Once your CDN is running, the next performance work is on the server side: database query time, PHP opcode caching, and response compression. Those topics — and full benchmarks comparing CMS platforms — are covered in the CMS performance benchmarks guide.
If you're also evaluating whether your current hosting setup is worth keeping or if a $5 VPS can actually handle real traffic, see the hosting cost and VPS comparison coming soon at host a CMS on a $5 VPS.
Want to see what UnfoldCMS features handle out of the box for performance and content delivery? The features page walks through caching, media, and API delivery in detail.
Related: Self-Hosted CMS: The Complete Guide for 2026 · Self-Hosted CMS Security: A Practical Guide · How to Set Up a Self-Hosted CMS
Free & Open Source
Own your CMS. No subscriptions.
Unfold CMS is free to download and self-host. Built on Laravel + React, full source code included.
Share this post: