WordPress Exposure
- SQL injection (MySQL database)
- PHP exploits (PHP execution)
- Plugin vulnerabilities (30+ plugins)
- Brute force login (wp-login.php)
- Malware injection (writable filesystem)
WordPress sites accumulate 15-30 plugins on average. Each plugin adds attack surface, update burden, performance cost, and licensing fees. When migrating to Cloudflare Pages, every plugin must be replaced, removed, or declared unnecessary.
This guide maps every common WordPress plugin category to its Cloudflare Pages alternative — with architecture diagrams, code examples, and migration effort ratings.
17 plugin categories across 4 migration phases. 5 of 17 require zero effort because the functionality is either unnecessary on static sites or already handled by Cloudflare’s infrastructure.
Security, Caching, Backup — not needed on static sites. No attack surface, no database, no PHP.
SEO, Analytics, Images, Social, Cookie Consent, Booking, Comments — already in HTML output or simple embeds.
Forms, Search, Slider/Gallery — implement alternative solutions with Cloudflare Functions or JS libraries.
Page Builder, Custom Post Types, Multilingual — significant development. Only when the site requires it.
Every common WordPress plugin category for medical practice sites, mapped to its Cloudflare Pages equivalent.
Per site in plugin licensing alone. At 10 sites: $21k—45k/yr including maintenance labor.
These plugins are unnecessary on a static site. Simply do not include them.
WordPress Exposure
Cloudflare Pages Exposure
| WordPress Plugin | Cloudflare Pages Equivalent | Action |
|---|---|---|
| Wordfence | Not needed | Remove |
| Sucuri | Not needed | Remove |
| iThemes Security | Not needed | Remove |
| All In One Security | Not needed | Remove |
| CleanTalk | Cloudflare Turnstile (forms only) | Replace if forms exist |
The only security configuration needed is a _headers file:
/* X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=() Strict-Transport-Security: max-age=31536000; includeSubDomains; preload Content-Security-Policy: frame-ancestors 'self'Annual cost saved: $100-600
WordPress caching plugins exist to work around the problem of PHP generating HTML on every request. Cloudflare Pages has no such problem — the HTML is pre-built and served directly from the CDN edge.
| WordPress Plugin | Cloudflare Pages Equivalent | Action |
|---|---|---|
| WP Rocket | Not needed | Remove |
| W3 Total Cache | Not needed | Remove |
| WP Super Cache | Not needed | Remove |
| Autoptimize | Not needed (or build-time) | Remove |
| Perfmatters | Build-time optimizations | Remove (apply at build) |
| LiteSpeed Cache | Not needed | Remove |
Annual cost saved: $50-250
| Feature | WordPress (UpdraftPlus) | Cloudflare Pages |
|---|---|---|
| File backup | Plugin zips wp-content | Git repository (every file versioned) |
| Database backup | Plugin dumps MySQL | No database to back up |
| Restore | Upload backup, hope it works | git revert or one-click rollback |
| Rollback speed | 10-30 minutes | < 30 seconds |
Every deployment is preserved indefinitely. You can roll back to any previous version at any time via the Cloudflare Dashboard.
Annual cost saved: $0-240
These carry over from WordPress HTML output with minimal changes.
Low Effort — SEO metadata is already in your WordPress HTML output. During migration, it carries over automatically.
| Feature | WordPress (Yoast/RankMath) | Cloudflare Pages |
|---|---|---|
| Title tags | Plugin-managed per page | Static <title> in HTML |
| Meta descriptions | Plugin field per page | Static <meta> in HTML |
| Open Graph tags | Auto-generated | Static <meta> in HTML |
| XML Sitemap | Auto-generated | Manual or build-time generated |
| robots.txt | Plugin-managed | Static file at /robots.txt |
| Structured data | Plugin fields | JSON-LD <script> blocks |
| Canonical URLs | Plugin-managed | Static <link rel="canonical"> |
| Redirects | Plugin or .htaccess | _redirects file or Functions |
Implementation: JSON-LD Structured Data
For medical practices, these schema types are most valuable:
<script type="application/ld+json">{ "@context": "https://schema.org", "@type": "MedicalBusiness", "name": "Your Practice Name", "image": "https://www.yourpractice.com/images/practice-photo.jpg", "url": "https://www.yourpractice.com", "telephone": "+1-408-555-0100", "address": { "@type": "PostalAddress", "streetAddress": "123 Main St, Suite 200", "addressLocality": "San Jose", "addressRegion": "CA", "postalCode": "95128", "addressCountry": "US" }, "openingHoursSpecification": [{ "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"], "opens": "09:00", "closes": "17:00" }], "medicalSpecialty": "Dermatology"}</script>For Astro sites, use @astrojs/sitemap to generate the XML sitemap automatically at build time.
Total migration time: 3-4 hours | Annual cost saved: $0-229
| Task | Time |
|---|---|
| Convert images to WebP during migration | Automated (build script) |
Add loading="lazy" to images | 30 min (find/replace) |
Set up responsive srcset | 1-2 hours |
| Total | 2-3 hours |
Cloudflare Image Optimization Options:
| Option | Cost | Best For |
|---|---|---|
| Build-time (Sharp) | Free | Migration projects |
Astro <Image> | Free | Fresh Astro builds |
| Cloudflare Polish | Free (Pro) or $5/mo | No code changes needed |
| Cloudflare Images | $5/mo + usage | Dynamic resize, many variants |
<!-- Google Analytics 4 - add to <head> on every page --><script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script><script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-XXXXXXXXXX');</script>Total migration time: 30 min - 2 hours | Annual cost saved: $0-400
Low Effort — No JavaScript needed. Use native platform share URLs:
<div class="share-buttons"> <a href="https://www.facebook.com/sharer/sharer.php?u=https://yoursite.com/post/" target="_blank" rel="noopener noreferrer">Share on Facebook</a> <a href="https://twitter.com/intent/tweet?url=https://yoursite.com/post/" target="_blank" rel="noopener noreferrer">Share on X</a> <a href="https://www.linkedin.com/sharing/share-offsite/?url=https://yoursite.com/post/" target="_blank" rel="noopener noreferrer">Share on LinkedIn</a></div>| Solution | Cost | GDPR | CCPA | Consent Logging |
|---|---|---|---|---|
| CookieConsent.js | Free | Yes | Yes | No (add yourself) |
| Osano | Free (1 domain) | Yes | Yes | Yes |
| CookieYes | $0-49/mo | Yes | Yes | Yes |
| Termly | Free (basic) | Yes | Yes | Yes |
If comments are needed, use Disqus or Giscus (GitHub-based, free).
These require implementing an alternative solution.
Forms are the most critical functionality for medical practice websites. Every site needs at least a contact form.
flowchart LR
A[User fills form] --> B[form-handler.js]
B --> C[POST /api/submit-form]
C --> D[Cloudflare Function]
D --> E[SendGrid API]
E --> F[Email delivered]
| Feature | WordPress (CF7/Gravity) | Cloudflare Pages |
|---|---|---|
| Form rendering | PHP-generated HTML | Static HTML |
| Form processing | PHP on server | Cloudflare Function (serverless) |
| Email delivery | wp_mail() / SMTP plugin | SendGrid API |
| Spam protection | Akismet, reCAPTCHA | Cloudflare Turnstile (free) |
| File uploads | Server filesystem | R2 Storage or Base64 in email |
| Cost | $0-259/year | $0 (all free tier) |
Migration effort:
| Task | Time |
|---|---|
| Create form HTML (per form) | 30-60 min |
| Write form-handler.js | 30 min (reusable) |
| Write submit-form.js Function | 1-2 hours (reusable) |
| Configure SendGrid | 30 min (one-time) |
| Add Turnstile CAPTCHA | 30 min (one-time) |
| Total (first form) | 3-5 hours |
| Each additional form | 1-2 hours |
Cloudflare Turnstile replaces reCAPTCHA with a privacy-friendly, free alternative. It is invisible to most users.
<!-- Add before the submit button --><div class="cf-turnstile" data-sitekey="0x4AAAAAAA..."></div><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>// In submit-form.js, verify the token:const turnstileToken = formData.get('cf-turnstile-response');const verifyResponse = await fetch( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: env.TURNSTILE_SECRET_KEY, response: turnstileToken, remoteip: request.headers.get('CF-Connecting-IP'), }), });| Service | Free Tier | Deliverability | Notes |
|---|---|---|---|
| SendGrid | 100 emails/day | High | Recommended, most documented |
| Resend | 3,000/month | High | Modern API, developer-friendly |
| Mailgun | 1,000/month (trial) | High | Good EU support |
| Postmark | 100/month | Very High | Best for transactional |
| Amazon SES | 3,000/month (from EC2) | High | Cheapest at scale |
| Approach | Complexity | Max Size | Best For |
|---|---|---|---|
| Base64 in email | Low | ~5 MB | Small files (photos, PDFs) |
| Cloudflare R2 Storage | Medium | 5 GB+ | Large files, medical records |
| Third-party (Uploadthing) | Low | Varies | Quick setup |
| Approach | Time | Best For |
|---|---|---|
| Pagefind (Recommended) | 2-3 hours | Most sites |
| Fuse.js | 1-2 hours | Small sites (< 50 pages) |
| Algolia | 4-6 hours | Large sites, advanced search |
| Remove search | 30 min | Sites where search is rarely used |
Pagefind is a static search library that builds an index at deploy time. No server, no API, no cost.
npm install -D pagefindnpx pagefind --site public --output-subdir _pagefind<link href="/_pagefind/pagefind-ui.css" rel="stylesheet"><div id="search"></div><script src="/_pagefind/pagefind-ui.js"></script><script> new PagefindUI({ element: '#search', showSubResults: true });</script>Free, open-source, tiny bundle (< 5 KB initial), handles 10,000+ pages.
Client-side fuzzy search for small sites:
<input type="text" id="search-input" placeholder="Search..."><div id="search-results"></div>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@7.0.0"></script><script> fetch('/search-index.json') .then(r => r.json()) .then(pages => { const fuse = new Fuse(pages, { keys: ['title', 'content', 'description'], threshold: 0.3, }); document.getElementById('search-input').addEventListener('input', (e) => { const results = fuse.search(e.target.value).slice(0, 10); document.getElementById('search-results').innerHTML = results .map(r => `<a href="${r.item.url}">${r.item.title}</a>`) .join(''); }); });</script>Hosted search for large sites needing instant results and faceted search.
| Feature | Free Tier | Paid |
|---|---|---|
| Records | 10,000 | Unlimited |
| Searches/month | 10,000 | Pay per use |
| Typo tolerance | Yes | Yes |
Setup requires an Algolia account and indexing your content via their API.
Low Effort — Replace the WordPress plugin with an embedded third-party booking widget.
| Service | Free Tier | Paid | Best For |
|---|---|---|---|
| Calendly | 1 event type | $10-16/mo | Simple scheduling |
| Acuity Scheduling | 1 calendar (trial) | $16-49/mo | Full-featured, medical |
| Cal.com | Unlimited (self-hosted) | $15/mo (hosted) | Open-source option |
| Square Appointments | Free (1 location) | $29+/mo | If already using Square |
| Jane App | N/A | $54-114/mo | Medical/health practices |
<!-- Calendly inline embed --><div class="calendly-inline-widget" data-url="https://calendly.com/your-practice/consultation" style="min-width:320px;height:630px;"></div><script src="https://assets.calendly.com/assets/external/widget.js" async></script><!-- Mailchimp signup form --><form action="https://yourpractice.us14.list-manage.com/subscribe/post?u=XXXXX&id=YYYYY" method="post" target="_blank"> <label for="mce-EMAIL">Subscribe to our newsletter:</label> <input type="email" name="EMAIL" id="mce-EMAIL" placeholder="your@email.com" required> <div style="position: absolute; left: -5000px;" aria-hidden="true"> <input type="text" name="b_XXXXX_YYYYY" tabindex="-1" value=""> </div> <button type="submit">Subscribe</button></form>For custom integration, use a Cloudflare Function to call the Mailchimp API directly.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
<div class="swiper" style="max-width: 800px; margin: 0 auto;"> <div class="swiper-wrapper"> <div class="swiper-slide"> <img src="/images/before-after-1.webp" alt="Result 1" loading="lazy"> </div> <div class="swiper-slide"> <img src="/images/before-after-2.webp" alt="Result 2" loading="lazy"> </div> </div> <div class="swiper-pagination"></div> <div class="swiper-button-prev"></div> <div class="swiper-button-next"></div></div>
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script><script> new Swiper('.swiper', { loop: true, pagination: { el: '.swiper-pagination', clickable: true }, navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' }, autoplay: { delay: 5000, disableOnInteraction: false }, });</script><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox@3/dist/css/glightbox.min.css">
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(250px,1fr)); gap:1rem;"> <a href="/images/gallery/full-1.webp" class="glightbox" data-gallery="results"> <img src="/images/gallery/thumb-1.webp" alt="Result 1" loading="lazy" style="width:100%; height:250px; object-fit:cover; border-radius:8px;"> </a> <!-- More items... --></div>
<script src="https://cdn.jsdelivr.net/npm/glightbox@3/dist/js/glightbox.min.js"></script><script>GLightbox({ selector: '.glightbox' });</script>Slider Libraries Comparison:
| Library | Size | Touch | Autoplay | Best For |
|---|---|---|---|---|
| Swiper.js | 140 KB | Yes | Yes | Full-featured sliders |
| Splide | 30 KB | Yes | Yes | Lightweight alternative |
| Keen Slider | 7 KB | Yes | Yes | Minimal bundle size |
| CSS-only | 0 KB | No | CSS animation | Simple auto-rotating |
These require significant development work and are only necessary for specific site types.
Multilingual is one of the most complex migrations because it touches every page and requires ongoing translation management.
| Task | Time |
|---|---|
| Set up folder structure or Astro i18n | 2-4 hours |
| Migrate translated content | 4-8 hours per language |
| Build language switcher | 1-2 hours |
| Add hreflang tags | 1 hour |
| Total | 10-20 hours |
public/├── en/│ ├── index.html│ ├── services/index.html│ └── contact/index.html├── es/│ ├── index.html│ ├── services/index.html│ └── contact/index.html└── index.html ← redirects to /en/ or detects languagesrc/├── pages/│ ├── en/│ │ ├── index.astro│ │ └── services.astro│ └── es/│ ├── index.astro│ └── servicios.astro├── i18n/│ ├── en.json│ └── es.json└── components/ └── LanguageSwitcher.astro{ "nav.home": "Home", "nav.services": "Services", "hero.cta": "Book Consultation" }{ "nav.home": "Inicio", "nav.services": "Servicios", "hero.cta": "Reservar Consulta" }Add hreflang tags to every page:
<head> <link rel="alternate" hreflang="en" href="https://yoursite.com/en/services/"> <link rel="alternate" hreflang="es" href="https://yoursite.com/es/servicios/"> <link rel="alternate" hreflang="x-default" href="https://yoursite.com/en/services/"></head>This is the biggest philosophical shift. You trade a visual editor for code-based components. The tradeoff: you lose drag-and-drop simplicity but gain performance, flexibility, and AI-compatibility.
| Divi/Elementor Module | Astro/Tailwind Equivalent |
|---|---|
| Hero Section | <HeroSection> component |
| Text Module | <p> with Tailwind typography classes |
| Image Module | <Image> from astro:assets |
| Button Module | <a> with Tailwind button classes |
| Blurb (icon + text) | <ServiceCard> component |
| Testimonial | <TestimonialCard> component |
| Accordion/Toggle | <details> + <summary> (native HTML) |
| Tabs | Alpine.js or CSS-only tabs |
| Contact Form | Custom form + form-handler.js |
| Map | Google Maps <iframe> embed |
| Before/After Slider | img-comparison-slider web component |
Total migration time: 8-20 hours | Annual cost saved: $90-250
This requires restructuring content from database records into Markdown/MDX files with typed frontmatter.
| Feature | WordPress (ACF/CPT) | Cloudflare Pages (Astro) |
|---|---|---|
| Custom fields | ACF field groups | Frontmatter schema (Zod) |
| Custom post types | register_post_type() | Content Collections |
| Repeater fields | ACF Repeater | Array fields in frontmatter |
| Query / filter | WP_Query | Astro getCollection() + filter |
// src/content/config.ts - replaces ACF field groupsimport { defineCollection, z } from 'astro:content';
const services = defineCollection({ type: 'content', schema: z.object({ title: z.string(), description: z.string(), image: z.string(), price: z.string().optional(), category: z.enum(['injectables', 'skin', 'body', 'laser']), featured: z.boolean().default(false), order: z.number(), }),});
export const collections = { services };---// Query and render - replaces WP_Queryimport { getCollection } from 'astro:content';const services = await getCollection('services');const featured = services.filter(s => s.data.featured).sort((a, b) => a.data.order - b.data.order);---
{featured.map(service => ( <ServiceCard title={service.data.title} href={`/services/${service.slug}/`} />))}Total migration time: 6-12 hours | Annual cost saved: $0-50
When migrating a WordPress site with many plugins, prioritize in this order:
Phase 1: Remove (Zero Effort) — Security, Caching, Backup, Database optimization, Login security. Simply do not include them.
Phase 2: Carry Over (Low Effort) — SEO meta tags (verify in HTML, 30 min), Analytics (add script tag, 15 min), Social share links (30 min), Cookie consent (30 min), Image lazy loading (30 min).
Phase 3: Replace (Medium Effort) — Forms (3-5 hours), Search (2-3 hours), Slider/Gallery (2-4 hours), Email marketing (1 hour), Booking (2-4 hours), Comments (remove or 1 hour).
Phase 4: Rebuild (High Effort, Only If Needed) — Page builder to Astro + Tailwind (8-20 hours), ACF/Custom Post Types to Content Collections (6-12 hours), Multilingual to Astro i18n (10-20 hours).
| Category | WordPress Annual Cost | Cloudflare Pages Cost |
|---|---|---|
| Security (Wordfence Pro) | $119 | $0 |
| Caching (WP Rocket) | $59 | $0 |
| SEO (Yoast Pro) | $99 | $0 |
| Forms (Gravity Forms) | $59-259 | $0 |
| Backup (UpdraftPlus Pro) | $70 | $0 |
| Image optimization (ShortPixel) | $40-100 | $0 |
| Analytics (MonsterInsights Pro) | $100-400 | $0 |
| Slider (Slider Revolution) | $25-35 | $0 |
| Cookie consent (CookieYes Pro) | $49-120 | $0 |
| Page builder (Divi) | $89 | $0 |
| Total | $709-1,511/year | $0/year |
| WordPress | Cloudflare Pages | |
|---|---|---|
| Annual plugin costs | $7,090-15,110 | $0 |
| Update maintenance (2 hrs/mo/site) | $12,000-24,000 | $0 |
| Security patching | $2,400-6,000 | $0 |
| Total | $21,490-45,110 | $0 |