Troubleshooting
This guide covers every common issue encountered during WordPress-to-Cloudflare-Pages migrations. Each issue includes the Symptom (what you see), Cause (why it happens), and Solution (how to fix it) with specific commands.
Cloudflare Pages Platform Issues
Section titled “Cloudflare Pages Platform Issues”These are known platform limitations documented by Cloudflare. Be aware of these before and during migration.
| Issue | Details | Workaround |
|---|---|---|
| Default Node.js is 12.18.0 | Extremely outdated; most frameworks need 18+ | Set NODE_VERSION=20 in environment variables |
| No incremental builds | Every build is a full rebuild | Optimize build scripts; batch content updates |
| Build timeout: 20 minutes | Large sites may hit this | Pre-process images; split heavy operations |
| 500 builds/month (free) | Per account, not per project | Upgrade to Workers Paid ($5/mo) for 5,000 builds |
| Functions won’t deploy via Dashboard | Known bug | Use Wrangler CLI or Git integration instead |
| *Can’t change .pages.dev subdomain | Permanent after first deploy | Delete and recreate project if needed |
| Issue | Details | Workaround |
|---|---|---|
| Git provider lock-in | Only GitHub and GitLab supported | Use Direct Upload (Wrangler CLI) as alternative |
| Can’t switch from Git to Direct Upload | Locked after first deploy method | Delete and recreate project to change method |
| Monorepo support limited | Root directory must be set correctly | Use root directory setting in build configuration |
| Issue | Details | Workaround |
|---|---|---|
| No wildcard custom domains | Can’t use *.domain.com | Add each subdomain individually |
| Worker routing conflicts | Domains with existing Workers can’t be used for Pages | Remove conflicting Worker routes first |
| Cloudflare Access conflicts | Domains with Access policies may have issues | Configure Access after Pages setup |
| 100 projects per account | Soft limit for portfolio-scale deployments | Request limit increase or use multiple accounts |
| Issue | Details | Workaround |
|---|---|---|
| 20,000 files per deploy (free) | Image-heavy WordPress sites may hit this | Use R2 for media; upgrade to Workers Paid (100K files) |
| 25 MB single file limit | Large PDFs and videos won’t deploy | Store in R2 and link from your site |
| 2,000 static redirects max | WordPress sites with long histories may hit this | Consolidate redirects; use dynamic redirects (100 limit) |
| 100 _headers rules max | Fine for most sites | Combine rules with wildcard paths |
| High-deployment project deletion | Projects with 100+ deployments may fail to delete | Contact Cloudflare support |
Migration Issues
Section titled “Migration Issues”403 Errors During Crawl
Section titled “403 Errors During Crawl”Symptom: Crawler returns 403 Forbidden on some or all pages when crawling the local WordPress site.
Cause: Security plugins (CleanTalk, Wordfence, iThemes Security, Sucuri) detect the crawler as a bot and block requests.
Solution:
# Temporarily disable the security plugin by renaming its foldercd /path/to/wordpress/wp-content/plugins/
# For CleanTalkmv cleantalk-spam-protect cleantalk-spam-protect.disabled
# For Wordfencemv wordfence wordfence.disabled
# Run your crawlnpm run build
# Re-enable after crawlmv cleantalk-spam-protect.disabled cleantalk-spam-protectmv wordfence.disabled wordfenceMissing CSS / Styles After Migration
Section titled “Missing CSS / Styles After Migration”Symptom: Pages load but appear unstyled — raw HTML text, no layout, no fonts, no colors.
Cause: CSS files were not synced from the WordPress installation, or CSS <link> paths were not rewritten to root-relative URLs.
Solution:
-
Check if CSS files exist in build output:
Terminal window ls -la sitename-static-build/wp-content/themes/yourtheme/style.css -
If missing, sync from WordPress:
Terminal window # Copy theme CSS from WordPress installationcp -r /path/to/wordpress/wp-content/themes/yourtheme/css/ \sitename-static-build/wp-content/themes/yourtheme/css/# Copy theme stylesheetcp /path/to/wordpress/wp-content/themes/yourtheme/style.css \sitename-static-build/wp-content/themes/yourtheme/style.css -
If CSS exists but not loading, check paths in HTML:
Terminal window # Look for absolute WordPress URLs in CSS referencesgrep -r "sitename.local" sitename-static-build/ --include="*.html" | grep -i "stylesheet" -
Fix paths to be root-relative:
Wrong: href="https://sitename.local/wp-content/themes/yourtheme/style.css"Right: href="/wp-content/themes/yourtheme/style.css"
Stray .local WordPress URLs in Output
Section titled “Stray .local WordPress URLs in Output”Symptom: Test suite fails with “Found stray WordPress URL” or clicking links navigates to sitename.local instead of staying on the static site.
Cause: The crawler did not rewrite all internal URLs. Common in inline JavaScript, data attributes, JSON-LD structured data, or Perfmatters cache paths.
Solution:
-
Find all occurrences:
Terminal window grep -rn "sitename.local" sitename-static-build/ --include="*.html" --include="*.css" --include="*.js" -
Replace in all files:
Terminal window # Replace with empty string (for absolute URLs that should be root-relative)find sitename-static-build/ -type f \( -name "*.html" -o -name "*.css" -o -name "*.js" \) \-exec sed -i '' 's|https://sitename.local||g' {} +# Also check for http:// variantfind sitename-static-build/ -type f \( -name "*.html" -o -name "*.css" -o -name "*.js" \) \-exec sed -i '' 's|http://sitename.local||g' {} + -
Handle Perfmatters cache paths specifically:
Terminal window find sitename-static-build/ -type f -name "*.html" \-exec sed -i '' 's|/wp-content/cache/perfmatters/sitename.local/|/wp-content/cache/perfmatters/site/|g' {} + -
Re-run tests to confirm:
Terminal window npm test
Missing Images / Media
Section titled “Missing Images / Media”Symptom: Broken image icons on pages. Images referenced in HTML do not load.
Cause: Media files from wp-content/uploads/ were not copied to the build output, or image paths still point to WordPress.
Solution:
-
Check what is referenced vs what exists:
Terminal window # Find all image references in HTMLgrep -roh 'src="[^"]*"' sitename-static-build/ --include="*.html" | \grep -i '\.\(jpg\|jpeg\|png\|gif\|webp\|svg\)' | sort -u# Check if uploads directory was syncedls sitename-static-build/wp-content/uploads/ -
Sync uploads from WordPress:
Terminal window # Copy entire uploads directoryrsync -av /path/to/wordpress/wp-content/uploads/ \sitename-static-build/wp-content/uploads/ -
If images were converted to WebP, verify WebP files exist:
Terminal window find sitename-static-build/ -name "*.webp" | head -20 -
Verify
WP_PUBLIC_DIRis set correctly in crawler config:Terminal window echo $WP_PUBLIC_DIR# Example: /Users/dev/Local Sites/sitename/app/public
Broken Internal Links
Section titled “Broken Internal Links”Symptom: Clicking navigation links or in-page links results in 404 errors.
Cause: URLs were not rewritten to match the static file structure, or pages were not crawled (depth limit, excluded by robots.txt, or behind JavaScript rendering).
Solution:
-
Identify broken links:
Terminal window npm run serve &npx linkinator http://localhost:4173/ --recurse -
Check if the target page was crawled:
Terminal window ls sitename-static-build/services/botox/index.html -
If page was not crawled, add it to the crawler’s start URLs:
// In crawler.js, add missing URLs to the seed listconst ADDITIONAL_URLS = ['/services/botox/','/services/fillers/','/gallery/page/2/']; -
Check for trailing slash issues:
# Cloudflare Pages serves:/about/ -> /about/index.html (works)/about -> /about/index.html (works if redirect configured)/about.html -> /about.html (different file)
Perfmatters / Caching Plugin Artifacts
Section titled “Perfmatters / Caching Plugin Artifacts”Symptom: References to Perfmatters cached CSS/JS files that do not exist in the build. Console errors about missing /wp-content/cache/perfmatters/ files.
Cause: WordPress caching plugins (Perfmatters, WP Rocket, Autoptimize) generate combined/minified CSS/JS files that are not part of the theme directory. The crawler captures HTML referencing these cached files but may not crawl the cache directory.
Solution:
-
Copy Perfmatters cache directory:
Terminal window cp -r /path/to/wordpress/wp-content/cache/perfmatters/ \sitename-static-build/wp-content/cache/perfmatters/ -
Fix hostname in cache paths (Perfmatters uses hostname in path):
Terminal window # Rename the hostname directory to "site"mv sitename-static-build/wp-content/cache/perfmatters/sitename.local/ \sitename-static-build/wp-content/cache/perfmatters/site/# Update all HTML referencesfind sitename-static-build/ -name "*.html" \-exec sed -i '' 's|/cache/perfmatters/sitename.local/|/cache/perfmatters/site/|g' {} + -
Alternative — inline critical CSS and remove Perfmatters references if the cached files are just concatenated CSS.
Third-Party Widget Issues
Section titled “Third-Party Widget Issues”Symptom: reCAPTCHA badges, UserWay accessibility widgets, cookie consent banners, or chat widgets do not appear or cause JavaScript errors.
Cause: These widgets load from external CDNs and may require specific initialization scripts that were deferred or removed during migration.
| Widget | Recommendation |
|---|---|
| reCAPTCHA | Remove entirely for static sites. Use Cloudflare Turnstile instead for bot protection. |
| UserWay | Remove the script tag. Consider Cloudflare’s accessibility options or a lighter alternative. |
| Cookie consent | Remove old plugin script. Add a lightweight banner like CookieConsent.js if legally required. |
| Chat widgets (Tidio, Intercom) | Keep the script tag — these load independently from external CDNs and usually work. |
| Google Analytics | Keep or replace with Cloudflare Web Analytics (free, privacy-friendly, no JS needed). |
To remove a widget:
# Find all references to the widgetgrep -rn "userway\|recaptcha\|cookie" sitename-static-build/ --include="*.html"
# Remove the script tags in the HTML files# (manual editing recommended -- be careful not to remove too much)Feed / oEmbed References Remaining
Section titled “Feed / oEmbed References Remaining”Symptom: HTML contains <link> tags pointing to /wp-json/, /feed/, /xmlrpc.php, or oEmbed discovery links.
Cause: WordPress adds these meta tags automatically. The crawler captures them but they serve no purpose on a static site and may expose WordPress origins.
Solution:
# Remove wp-json/oembed links from all HTML filesfind sitename-static-build/ -name "*.html" -exec sed -i '' \ -e '/<link[^>]*wp-json[^>]*>/d' \ -e '/<link[^>]*oembed[^>]*>/d' \ -e '/<link[^>]*xmlrpc[^>]*>/d' \ -e '/<link[^>]*wlwmanifest[^>]*>/d' \ -e '/<link[^>]*EditURI[^>]*>/d' \ {} +
# Remove RSS feed links (optional -- unless you want to keep feed)find sitename-static-build/ -name "*.html" -exec sed -i '' \ '/<link[^>]*type="application\/rss+xml"[^>]*>/d' {} +Verify removal:
grep -rn "wp-json\|oembed\|xmlrpc\|wlwmanifest" sitename-static-build/ --include="*.html"# Should return no resultsForm Issues
Section titled “Form Issues”Forms Not Submitting
Section titled “Forms Not Submitting”Symptom: Clicking the submit button does nothing, or the browser shows a JavaScript error in the console.
Cause: The form handler JavaScript is not loaded, has a syntax error, or the form’s action attribute points to the wrong endpoint.
Solution:
-
Check browser console for errors: Open DevTools (F12) > Console tab. Submit the form and look for red error messages.
-
Verify form handler script is loaded:
Terminal window grep -n "form-handler" public/your-page/index.html -
Verify the form action/endpoint:
<!-- Form should POST to the Functions endpoint --><form id="contact-form" method="POST"><!-- Fields... --><button type="submit">Submit</button></form><!-- JavaScript should handle submission via fetch() --><script src="form-handler.js"></script> -
Verify the Functions file exists:
Terminal window ls functions/api/submit-form.js -
Test the endpoint directly:
Terminal window curl -X POST http://localhost:8788/api/submit-form \-F "name=Test" \-F "email=test@example.com" \-F "phone=555-0000" \-F "message=Test submission"
No Email Received After Submission
Section titled “No Email Received After Submission”Symptom: Form submits successfully (200 response, success modal shows), but no email arrives.
Cause: SendGrid API key not configured, sender not verified, or email going to spam.
Solution:
-
Check SendGrid API key is set: In Cloudflare Dashboard: Pages > [Project] > Settings > Environment Variables. Verify
SENDGRID_API_KEYexists and is not empty. -
Verify sender email is verified in SendGrid: Go to https://app.sendgrid.com/settings/sender_auth. Confirm the “from” email is verified. If not verified, emails will silently fail.
-
Check SendGrid activity log: Go to https://app.sendgrid.com/email_activity. Look for recent send attempts. Check for bounces or blocks.
-
Check Cloudflare Functions logs:
Terminal window npx wrangler pages deployment tail -
Test locally first:
Terminal window npx wrangler pages dev public# Submit form -- check terminal for "DEVELOPMENT MODE" email output
Modal Not Appearing After Submit
Section titled “Modal Not Appearing After Submit”Symptom: Form submits and email is received, but the “Thank You” success modal does not display.
Cause: The modal HTML is missing from the page, the JavaScript to show it has an error, or CSS is hiding it incorrectly.
Solution:
-
Verify modal HTML exists in the page:
<div id="form-success-modal" class="form-modal-overlay" style="display:none;"><div class="form-modal-content"><button class="form-modal-close">×</button><h2>Thank You!</h2><p>Your form has been submitted successfully.</p></div></div> -
Verify JavaScript shows the modal on success:
const modal = document.getElementById('form-success-modal');if (modal) {modal.style.display = 'flex';} -
Check for CSS conflicts:
Terminal window grep -n "form-modal\|form-success" public/your-page/index.html -
Test by manually showing the modal in DevTools console:
document.getElementById('form-success-modal').style.display = 'flex';
CORS Errors on Form Submission
Section titled “CORS Errors on Form Submission”Symptom: Browser console shows: Access to fetch at '/api/submit-form' from origin 'https://yoursite.com' has been blocked by CORS policy.
Cause: The Cloudflare Pages Function is missing CORS headers, or the OPTIONS preflight handler is not implemented.
Solution:
Verify your functions/api/submit-form.js includes both the CORS headers in responses AND the OPTIONS handler:
// Every response must include CORS headersreturn new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', },});
// OPTIONS handler for CORS preflight (required)export async function onRequestOptions() { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Max-Age': '86400', }, });}After fixing, redeploy:
npm run deploySendGrid API Key Not Configured
Section titled “SendGrid API Key Not Configured”Symptom: Form returns 500 error. Cloudflare Functions log shows: SENDGRID_API_KEY not configured.
Cause: The environment variable was not set in Cloudflare Pages settings, or the project was not redeployed after adding it.
Solution:
-
Set the environment variable: Cloudflare Dashboard > Pages > [Your Project] > Settings > Environment Variables. Add
SENDGRID_API_KEYwith the valueSG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Select Production (and Preview if you want staging to work too). Save. -
Redeploy (environment variables only take effect on new deployments):
Terminal window npm run deploy -
Verify it works:
Terminal window npx wrangler pages deployment tail
Forms Only Work on Custom Domain (Not *.pages.dev)
Section titled “Forms Only Work on Custom Domain (Not *.pages.dev)”Symptom: Forms work at https://yoursite.com but fail at https://project.pages.dev with a SendGrid error about unauthorized sender.
Cause: SendGrid requires the “from” email domain to be verified. If your “from” email is noreply@yoursite.com, SendGrid will reject sends when the request originates from a different domain. Additionally, some email services like MailChannels require DNS records (SPF/DKIM) that only work with your custom domain.
Solution:
Use your custom domain for production form testing. For development/preview deployments, the form handler should detect localhost and log instead of sending:
const isDevelopment = request.url.includes('localhost') || request.url.includes('127.0.0.1');
if (isDevelopment) { console.log('=== DEVELOPMENT MODE: Email would be sent ==='); console.log('To:', RECIPIENT_EMAILS.join(', ')); console.log('Body:\n' + emailBody); // Return success without sending} else { // Send via SendGrid}Email Going to Spam
Section titled “Email Going to Spam”Symptom: Form submissions are sent but land in recipients’ spam/junk folders.
Cause: Missing or incorrect email authentication records (SPF, DKIM, DMARC) for the sending domain.
Solution:
-
Set up SPF record:
Type: TXTName: @Content: v=spf1 include:sendgrid.net ~all -
Set up DKIM (via SendGrid): SendGrid Dashboard > Settings > Sender Authentication > Domain Authentication. Follow the steps to add CNAME records to your DNS.
-
Set up DMARC:
Type: TXTName: _dmarcContent: v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com -
Use a verified “from” address that matches your domain (not
gmail.comoryahoo.com). -
Test deliverability: Send a test email to https://www.mail-tester.com/ address. Score should be 8+ out of 10.
Duplicate Form Submissions
Section titled “Duplicate Form Submissions”Symptom: Recipient receives the same form submission email 2-3 times.
Cause: User double-clicks the submit button, or the form handler does not disable the button during submission.
Solution:
Add submit button disabling to the form handler JavaScript:
const form = document.getElementById('contact-form');const submitBtn = form.querySelector('button[type="submit"]');
form.addEventListener('submit', async (e) => { e.preventDefault();
// Prevent double submission if (submitBtn.disabled) return; submitBtn.disabled = true; submitBtn.textContent = 'Sending...';
try { const formData = new FormData(form); const response = await fetch('/api/submit-form', { method: 'POST', body: formData, });
const data = await response.json();
if (data.success) { // Show success modal document.getElementById('form-success-modal').style.display = 'flex'; form.reset(); } else { alert('Error: ' + data.message); } } catch (error) { alert('Network error. Please try again.'); } finally { submitBtn.disabled = false; submitBtn.textContent = 'Submit'; }});Deployment Issues
Section titled “Deployment Issues”npm run deploy Fails
Section titled “npm run deploy Fails”Symptom: Deployment command exits with an error.
Cause: Multiple possible causes — authentication, missing files, wrangler configuration.
Solution:
-
Check authentication:
Terminal window npx wrangler whoami# If not authenticated:npx wrangler login -
Check wrangler.toml exists and is valid:
wrangler.toml name = "your-project-name"compatibility_date = "2024-01-01"pages_build_output_dir = "public" -
Check the output directory exists:
Terminal window ls -la public/# Or for migration projects:ls -la sitename-static-build/ -
Deploy with verbose output:
Terminal window npx wrangler pages deploy public --project-name your-project-name 2>&1
Wrangler Authentication Errors
Section titled “Wrangler Authentication Errors”Symptom: Error: You must be logged in to use this command or Authentication error.
Cause: OAuth token expired or wrangler is not authenticated.
Solution:
# Re-authenticatenpx wrangler login
# Verify authenticationnpx wrangler whoami# Expected output: your account name and account ID
# If using CI/CD, use API token instead:CLOUDFLARE_API_TOKEN=your_token npx wrangler pages deploy publicBuild Failures
Section titled “Build Failures”Symptom: npm run build (crawler) fails with errors.
Cause: WordPress site not accessible, network issues, or missing dependencies.
Solution:
-
Verify WordPress is running:
Terminal window curl -s -o /dev/null -w "%{http_code}" https://sitename.local/# Should return 200 -
Check for SSL certificate issues:
Terminal window # If using self-signed cert (Local by Flywheel), the crawler must ignore SSL errors# In crawler.js, ensure:# process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -
Check dependencies are installed:
Terminal window npm install -
Check Node.js version:
Terminal window node --version# Should be 18+ (LTS)
Changes Not Appearing After Deploy (CDN Cache)
Section titled “Changes Not Appearing After Deploy (CDN Cache)”Symptom: You deployed a change but the live site still shows the old version.
Cause: Cloudflare’s CDN is serving cached content. Browsers may also cache aggressively due to Cache-Control headers.
Solution:
-
Purge Cloudflare cache:
Terminal window # Via Cloudflare Dashboard:# Caching > Configuration > Purge Everything# Via API:curl -X POST "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/purge_cache" \-H "Authorization: Bearer YOUR_API_TOKEN" \-H "Content-Type: application/json" \--data '{"purge_everything":true}' -
Hard refresh browser: Mac:
Cmd+Shift+R/ Windows:Ctrl+Shift+R. Or open Incognito/Private window. -
Verify the correct deployment is active:
Terminal window npx wrangler pages deployment list --project-name your-project-name -
For persistent caching issues, check
_headersfile:.html # Reduce HTML cache time if changes need to appear quicklyCache-Control: public, max-age=300
Wrong Files Deployed
Section titled “Wrong Files Deployed”Symptom: The deployed site shows wrong content, a default page, or files from a different project.
Cause: The pages_build_output_dir in wrangler.toml points to the wrong directory, or you ran the deploy command from the wrong directory.
Solution:
-
Check wrangler.toml:
Terminal window cat wrangler.toml# Verify pages_build_output_dir matches your actual output folder -
Check the deploy command:
Terminal window # Explicit directory in deploy command:npx wrangler pages deploy public --project-name your-project-name# Or for migration projects:npx wrangler pages deploy sitename-static-build --project-name your-project-name -
Verify correct content in the output directory:
Terminal window head -5 public/index.html# Should show YOUR site's HTML, not a template or placeholder
Environment Variables Not Available in Functions
Section titled “Environment Variables Not Available in Functions”Symptom: env.SENDGRID_API_KEY is undefined inside the Cloudflare Pages Function.
Cause: Environment variables were set after the last deployment. Variables only take effect on new deployments.
Solution:
-
Confirm the variable is set in the dashboard: Pages > [Project] > Settings > Environment Variables. Check the variable exists for the correct environment (Production/Preview).
-
Redeploy to pick up the variable:
Terminal window npm run deploy -
Access variables correctly in the Function:
export async function onRequestPost(context) {const { request, env } = context;// Correct: access via env objectconst apiKey = env.SENDGRID_API_KEY;// Wrong: process.env does not work in Cloudflare Functions// const apiKey = process.env.SENDGRID_API_KEY; // undefined}
Performance Issues
Section titled “Performance Issues”Lighthouse Score Lower Than Expected
Section titled “Lighthouse Score Lower Than Expected”Symptom: Lighthouse Performance score is below 90 on mobile or below 95 on desktop.
Cause: Usually a combination of unoptimized images, render-blocking CSS/JS, and third-party scripts.
Solution — follow this priority order:
| Priority | Issue | Fix | Impact |
|---|---|---|---|
| 1 | Unoptimized images | Convert to WebP, add width/height, use loading="lazy" | High |
| 2 | Render-blocking CSS | Inline critical CSS in <head>, defer non-critical | High |
| 3 | Render-blocking JS | Add defer or async to <script> tags | High |
| 4 | Third-party scripts | Remove analytics/trackers or delay loading | Medium |
| 5 | Large DOM | Remove unnecessary HTML elements | Medium |
| 6 | Missing text compression | Cloudflare Brotli (enabled by default) | Low |
# After each fix, re-run Lighthouse to measure improvementnpx lighthouse http://localhost:4173/ \ --only-categories=performance \ --output=json \ --output-path=./lighthouse-after-fix.json \ --chrome-flags="--headless"
# Extract scorecat lighthouse-after-fix.json | jq '.categories.performance.score * 100 | floor'Images Loading Slowly
Section titled “Images Loading Slowly”Symptom: Images are the last elements to appear. LCP is high because the hero image loads late.
Cause: Images are in original format (JPEG/PNG), oversized, or all loading eagerly.
Solution:
-
Convert images to WebP using sharp (already in project dependencies).
-
Add explicit dimensions to prevent CLS:
<!-- Before (causes layout shift) --><img src="/images/hero.webp" alt="Hero"><!-- After (no layout shift) --><img src="/images/hero.webp" alt="Hero" width="1200" height="600"> -
Lazy load below-the-fold images:
<!-- Hero image: load eagerly (above fold) --><img src="/images/hero.webp" alt="Hero" width="1200" height="600"loading="eager" fetchpriority="high"><!-- Below-fold images: lazy load --><img src="/images/team.webp" alt="Team" width="800" height="400"loading="lazy">
JavaScript Blocking Render
Section titled “JavaScript Blocking Render”Symptom: Lighthouse flags “Eliminate render-blocking resources” with JavaScript files listed.
Cause: <script> tags in the <head> without defer or async block HTML parsing.
Solution:
<!-- Before (render-blocking) --><script src="/scripts/main.js"></script>
<!-- After (non-blocking, executes after HTML parsed) --><script src="/scripts/main.js" defer></script>
<!-- Or for scripts that don't depend on DOM order --><script src="/scripts/analytics.js" async></script>For inline scripts that should be delayed:
<script> function loadDelayed() { // Analytics, chat widgets, etc. } document.addEventListener('mousemove', loadDelayed, { once: true }); document.addEventListener('scroll', loadDelayed, { once: true }); document.addEventListener('touchstart', loadDelayed, { once: true });</script>Fonts Causing Layout Shift (CLS)
Section titled “Fonts Causing Layout Shift (CLS)”Symptom: Text jumps/resizes after the page initially renders. CLS score is above 0.1.
Cause: Web fonts load after system fonts, causing text to reflow when the custom font replaces the fallback.
Solution:
-
Use
font-display: swapin @font-face:@font-face {font-family: 'CustomFont';src: url('/fonts/custom.woff2') format('woff2');font-display: swap;} -
Preload critical fonts:
<link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossorigin> -
Use size-adjusted fallback fonts:
@font-face {font-family: 'CustomFont Fallback';src: local('Arial');size-adjust: 105%;ascent-override: 90%;descent-override: 22%;line-gap-override: 0%;}body {font-family: 'CustomFont', 'CustomFont Fallback', Arial, sans-serif;}
Third-Party Scripts Dragging Performance
Section titled “Third-Party Scripts Dragging Performance”Symptom: TBT (Total Blocking Time) is high. Lighthouse shows third-party scripts consuming main thread time.
Cause: Analytics, marketing pixels, chat widgets, and tracking scripts execute during page load.
| Script | Action | How |
|---|---|---|
| Google Analytics | Replace with Cloudflare Web Analytics | Remove GA script, enable in Cloudflare Dashboard |
| Facebook Pixel | Remove or delay | Remove script tag entirely for static brochure sites |
| HotJar / FullStory | Remove | Not needed for static sites |
| Chat widget | Delay until interaction | Load script on first user interaction |
| reCAPTCHA | Replace with Cloudflare Turnstile | Lighter, privacy-friendly alternative |
Delayed loading pattern:
<script> function loadChatWidget() { const script = document.createElement('script'); script.src = 'https://chat-widget.example.com/widget.js'; document.body.appendChild(script); } // Only load after 5 seconds or user interaction setTimeout(loadChatWidget, 5000);</script>DNS / Domain Issues
Section titled “DNS / Domain Issues”DNS Not Propagating
Section titled “DNS Not Propagating”Symptom: Domain still shows old site hours after DNS change.
Cause: DNS TTL has not expired, or ISP/local DNS resolver is caching the old record.
Solution:
-
Check current DNS resolution:
Terminal window # Check authoritative DNSdig yoursite.com +trace# Check from Google's DNSdig @8.8.8.8 yoursite.com# Check from Cloudflare's DNSdig @1.1.1.1 yoursite.com -
Flush local DNS cache:
Terminal window # macOSsudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder# Windowsipconfig /flushdns -
Wait for TTL to expire. If the old DNS had a 24-hour TTL, propagation can take up to 24 hours. Plan DNS changes with low TTL in advance:
# 1-2 days before migration: lower TTL to 300 seconds# Then make the DNS change# After verification: raise TTL back to 3600 or higher
SSL Certificate Errors
Section titled “SSL Certificate Errors”Symptom: Browser shows “Your connection is not private” or “ERR_CERT_COMMON_NAME_INVALID”.
Cause: Cloudflare has not yet provisioned the SSL certificate for the custom domain, or the domain was just added.
Solution:
-
Wait for automatic provisioning (usually 1-15 minutes): Cloudflare Dashboard > SSL/TLS > Edge Certificates. Check if a certificate is listed for your domain.
-
Verify domain is properly configured: Pages > [Project] > Custom domains. Domain should show “Active” status.
-
If using Full (Strict) SSL mode with an origin server: Cloudflare Dashboard > SSL/TLS > Overview. Set SSL/TLS encryption mode to “Full” (not “Full (Strict)”). For Pages-only sites, SSL is handled automatically.
Mixed Content Warnings
Section titled “Mixed Content Warnings”Symptom: Browser console shows mixed content warnings. Some resources load over HTTP instead of HTTPS.
Cause: HTML contains http:// references to assets instead of https:// or protocol-relative URLs.
Solution:
# Find all http:// references in HTML (excluding safe ones)grep -rn 'http://' sitename-static-build/ --include="*.html" | \ grep -v 'http://www.w3.org' | \ grep -v 'http://schema.org'
# Replace http:// with https:// for your domainfind sitename-static-build/ -name "*.html" \ -exec sed -i '' 's|http://yoursite.com|https://yoursite.com|g' {} +
# Or replace with protocol-relative URLsfind sitename-static-build/ -name "*.html" \ -exec sed -i '' 's|http://yoursite.com|//yoursite.com|g' {} +Domain Not Resolving
Section titled “Domain Not Resolving”Symptom: ERR_NAME_NOT_RESOLVED or DNS_PROBE_FINISHED_NXDOMAIN.
Cause: DNS records not configured, nameservers not pointing to Cloudflare, or domain expired.
Solution:
-
Verify domain is added to Cloudflare: Dashboard > Websites > [Your Domain]. Status should be “Active”.
-
Verify nameservers:
Terminal window dig yoursite.com NS# Should show Cloudflare nameservers -
Verify DNS records exist:
Terminal window dig yoursite.com Adig www.yoursite.com CNAME -
For Pages custom domains, verify CNAME record:
Type: CNAMEName: @ (or www)Target: your-project.pages.devProxy: Enabled (orange cloud)
Redirect Loops
Section titled “Redirect Loops”Symptom: Browser shows “ERR_TOO_MANY_REDIRECTS”.
Cause: Conflicting redirect rules between Cloudflare SSL settings, Page Rules, and _redirects file.
Solution:
-
Check SSL mode: Cloudflare Dashboard > SSL/TLS > Overview. Set to “Full” (not “Flexible”). “Flexible” can cause loops because Cloudflare connects to origin via HTTP, origin redirects to HTTPS, repeat.
-
Check for conflicting redirects:
Terminal window cat public/_redirects# Also check Page Rules in Cloudflare Dashboard > Rules > Page Rules -
Check “Always Use HTTPS” setting: SSL/TLS > Edge Certificates > Always Use HTTPS. This should be ON for production but can conflict with other redirect rules.
-
Clear browser cookies and cache (redirect loops can be cached by the browser). Clear cookies for the specific domain or test in Incognito/Private window.
Development Environment Issues
Section titled “Development Environment Issues”Port 8788 Already in Use
Section titled “Port 8788 Already in Use”Symptom: npx wrangler pages dev public fails with “Address already in use” or “EADDRINUSE”.
Cause: A previous Wrangler process or another server is still running on port 8788.
Solution:
# Find the process using port 8788lsof -i :8788
# Kill itkill -9 $(lsof -ti:8788)
# Or use a different portnpx wrangler pages dev public --port 8789npm Install Failures
Section titled “npm Install Failures”Symptom: npm install fails with permission errors, network errors, or dependency conflicts.
Cause: Node.js version mismatch, npm cache corruption, or OS-level permission issues.
Solution:
-
Clear npm cache:
Terminal window npm cache clean --forcerm -rf node_modules package-lock.jsonnpm install -
Check Node.js version:
Terminal window node --version# Must be 18+ for Wrangler and modern dependencies -
Fix permission errors (macOS/Linux):
Terminal window sudo chown -R $(whoami) ~/.npm -
If sharp fails to install (common on Apple Silicon):
Terminal window npm install --platform=darwin --arch=arm64 sharp -
If Playwright fails to install browsers:
Terminal window npx playwright install chromium# If that fails:npx playwright install --with-deps chromium
Wrangler Version Incompatibility
Section titled “Wrangler Version Incompatibility”Symptom: Wrangler commands fail with unexpected errors, or features described in documentation do not work.
Cause: Outdated Wrangler version installed globally or locally.
Solution:
# Check installed versionnpx wrangler --version
# Update global installationnpm install -g wrangler@latest
# Update local installationnpm install wrangler@latest --save-dev
# If using both global and local, local takes precedence via npxnpx wrangler --versionPlaywright Browser Not Installed
Section titled “Playwright Browser Not Installed”Symptom: Visual diff or download script fails with: browserType.launch: Executable doesn't exist at /path/to/chromium.
Cause: Playwright was installed as an npm package, but the browser binaries were not downloaded.
Solution:
# Install Chromium binarynpx playwright install chromium
# If that fails with permission errors:npx playwright install --with-deps chromium
# Verify installationnpx playwright --versionEmergency Procedures
Section titled “Emergency Procedures”Site Is Down: Immediate Rollback
Section titled “Site Is Down: Immediate Rollback”Symptom: Production site returns errors, blank page, or is completely unreachable after a deployment.
Cause: Bad deployment, broken build, or configuration error.
Solution (Cloudflare Pages rollback):
-
List recent deployments:
Terminal window npx wrangler pages deployment list --project-name your-project-name -
Find the last known good deployment ID — look for the deployment before the broken one.
-
Rollback via Cloudflare Dashboard (fastest): Pages > [Project] > Deployments > Click on last good deployment > “Rollback to this deployment”.
Alternative (redeploy last known good code):
# If you have the last working version in git:git log --oneline -10git checkout <last-good-commit>npm run deploy
# Return to latest:git checkout mainForms Broken in Production
Section titled “Forms Broken in Production”Critical Patients cannot submit inquiries.
-
Immediate diagnostic:
Terminal window curl -X POST https://yoursite.com/api/submit-form \-F "name=Emergency Test" \-F "email=admin@yoursite.com" \-F "phone=555-0000" \-F "message=Testing form endpoint"# 200 + success=true = endpoint works, issue is client-side JS# 500 = server-side error (SendGrid, env vars)# 404 = Functions not deployed -
Check Functions logs:
Terminal window npx wrangler pages deployment tail --project-name your-project-name -
If SendGrid key expired: Log into SendGrid, generate new API key. Update in Cloudflare Dashboard > Pages > Settings > Environment Variables. Redeploy:
npm run deploy. -
If Functions missing (404 on /api/submit-form):
Terminal window ls functions/api/submit-form.jsnpm run deploy -
Temporary workaround while fixing: Add a
mailto:link or phone number prominently on the page so patients can still reach you.
Accidentally Deployed Wrong Version
Section titled “Accidentally Deployed Wrong Version”-
Rollback immediately: Cloudflare Dashboard > Pages > [Project] > Deployments. Click the previous correct deployment. Click “Rollback to this deployment”.
-
Verify you are deploying the right project:
Terminal window pwdcat wrangler.toml | grep namegit branch --show-currentnpx wrangler pages deploy public --project-name correct-project-name
Cache Purge Procedure
Section titled “Cache Purge Procedure”curl -X POST "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/purge_cache" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"purge_everything":true}'curl -X POST "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/purge_cache" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{ "files": [ "https://yoursite.com/", "https://yoursite.com/contact/", "https://yoursite.com/services/" ] }'- Cloudflare Dashboard > Caching > Configuration
- Click “Purge Everything” or “Custom Purge”
- Enter specific URLs if doing a targeted purge
After purging: Verify the fix by hard-refreshing the browser (Cmd+Shift+R / Ctrl+Shift+R) or testing in an incognito window.
Quick Reference: Diagnostic Commands
Section titled “Quick Reference: Diagnostic Commands”# Check if site is upcurl -s -o /dev/null -w "%{http_code}" https://yoursite.com/
# Check DNS resolutiondig yoursite.com A +short
# Check SSL certificateopenssl s_client -connect yoursite.com:443 -servername yoursite.com < /dev/null 2>/dev/null | openssl x509 -noout -dates
# Check security headerscurl -s -D - https://yoursite.com/ -o /dev/null | head -30
# Check Cloudflare statuscurl -s https://www.cloudflarestatus.com/api/v2/summary.json | jq '.status.description'
# Check wrangler authnpx wrangler whoami
# List deploymentsnpx wrangler pages deployment list --project-name your-project-name
# View Functions logsnpx wrangler pages deployment tail --project-name your-project-name
# Test form endpointcurl -X POST https://yoursite.com/api/submit-form \ -F "name=Test" -F "email=test@test.com"
# Find process on portlsof -i :8788
# Kill process on portkill -9 $(lsof -ti:8788)