Skip to content

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.


These are known platform limitations documented by Cloudflare. Be aware of these before and during migration.

IssueDetailsWorkaround
Default Node.js is 12.18.0Extremely outdated; most frameworks need 18+Set NODE_VERSION=20 in environment variables
No incremental buildsEvery build is a full rebuildOptimize build scripts; batch content updates
Build timeout: 20 minutesLarge sites may hit thisPre-process images; split heavy operations
500 builds/month (free)Per account, not per projectUpgrade to Workers Paid ($5/mo) for 5,000 builds
Functions won’t deploy via DashboardKnown bugUse Wrangler CLI or Git integration instead
*Can’t change .pages.dev subdomainPermanent after first deployDelete and recreate project if needed

Common

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:

Terminal window
# Temporarily disable the security plugin by renaming its folder
cd /path/to/wordpress/wp-content/plugins/
# For CleanTalk
mv cleantalk-spam-protect cleantalk-spam-protect.disabled
# For Wordfence
mv wordfence wordfence.disabled
# Run your crawl
npm run build
# Re-enable after crawl
mv cleantalk-spam-protect.disabled cleantalk-spam-protect
mv wordfence.disabled wordfence

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:

  1. Check what is referenced vs what exists:

    Terminal window
    # Find all image references in HTML
    grep -roh 'src="[^"]*"' sitename-static-build/ --include="*.html" | \
    grep -i '\.\(jpg\|jpeg\|png\|gif\|webp\|svg\)' | sort -u
    # Check if uploads directory was synced
    ls sitename-static-build/wp-content/uploads/
  2. Sync uploads from WordPress:

    Terminal window
    # Copy entire uploads directory
    rsync -av /path/to/wordpress/wp-content/uploads/ \
    sitename-static-build/wp-content/uploads/
  3. If images were converted to WebP, verify WebP files exist:

    Terminal window
    find sitename-static-build/ -name "*.webp" | head -20
  4. Verify WP_PUBLIC_DIR is set correctly in crawler config:

    Terminal window
    echo $WP_PUBLIC_DIR
    # Example: /Users/dev/Local Sites/sitename/app/public

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:

  1. Identify broken links:

    Terminal window
    npm run serve &
    npx linkinator http://localhost:4173/ --recurse
  2. Check if the target page was crawled:

    Terminal window
    ls sitename-static-build/services/botox/index.html
  3. If page was not crawled, add it to the crawler’s start URLs:

    // In crawler.js, add missing URLs to the seed list
    const ADDITIONAL_URLS = [
    '/services/botox/',
    '/services/fillers/',
    '/gallery/page/2/'
    ];
  4. 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)

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:

  1. Copy Perfmatters cache directory:

    Terminal window
    cp -r /path/to/wordpress/wp-content/cache/perfmatters/ \
    sitename-static-build/wp-content/cache/perfmatters/
  2. 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 references
    find sitename-static-build/ -name "*.html" \
    -exec sed -i '' 's|/cache/perfmatters/sitename.local/|/cache/perfmatters/site/|g' {} +
  3. Alternative — inline critical CSS and remove Perfmatters references if the cached files are just concatenated CSS.


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.

WidgetRecommendation
reCAPTCHARemove entirely for static sites. Use Cloudflare Turnstile instead for bot protection.
UserWayRemove the script tag. Consider Cloudflare’s accessibility options or a lighter alternative.
Cookie consentRemove 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 AnalyticsKeep or replace with Cloudflare Web Analytics (free, privacy-friendly, no JS needed).

To remove a widget:

Terminal window
# Find all references to the widget
grep -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)

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:

Terminal window
# Remove wp-json/oembed links from all HTML files
find 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:

Terminal window
grep -rn "wp-json\|oembed\|xmlrpc\|wlwmanifest" sitename-static-build/ --include="*.html"
# Should return no results

Critical

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:

  1. Check browser console for errors: Open DevTools (F12) > Console tab. Submit the form and look for red error messages.

  2. Verify form handler script is loaded:

    Terminal window
    grep -n "form-handler" public/your-page/index.html
  3. 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>
  4. Verify the Functions file exists:

    Terminal window
    ls functions/api/submit-form.js
  5. 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"

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:

  1. Set the environment variable: Cloudflare Dashboard > Pages > [Your Project] > Settings > Environment Variables. Add SENDGRID_API_KEY with the value SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Select Production (and Preview if you want staging to work too). Save.

  2. Redeploy (environment variables only take effect on new deployments):

    Terminal window
    npm run deploy
  3. 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
}

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:

  1. Set up SPF record:

    Type: TXT
    Name: @
    Content: v=spf1 include:sendgrid.net ~all
  2. Set up DKIM (via SendGrid): SendGrid Dashboard > Settings > Sender Authentication > Domain Authentication. Follow the steps to add CNAME records to your DNS.

  3. Set up DMARC:

    Type: TXT
    Name: _dmarc
    Content: v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
  4. Use a verified “from” address that matches your domain (not gmail.com or yahoo.com).

  5. Test deliverability: Send a test email to https://www.mail-tester.com/ address. Score should be 8+ out of 10.


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';
}
});

Symptom: Deployment command exits with an error.

Cause: Multiple possible causes — authentication, missing files, wrangler configuration.

Solution:

  1. Check authentication:

    Terminal window
    npx wrangler whoami
    # If not authenticated:
    npx wrangler login
  2. Check wrangler.toml exists and is valid:

    wrangler.toml
    name = "your-project-name"
    compatibility_date = "2024-01-01"
    pages_build_output_dir = "public"
  3. Check the output directory exists:

    Terminal window
    ls -la public/
    # Or for migration projects:
    ls -la sitename-static-build/
  4. Deploy with verbose output:

    Terminal window
    npx wrangler pages deploy public --project-name your-project-name 2>&1

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:

  1. Check wrangler.toml:

    Terminal window
    cat wrangler.toml
    # Verify pages_build_output_dir matches your actual output folder
  2. 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
  3. 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:

  1. Confirm the variable is set in the dashboard: Pages > [Project] > Settings > Environment Variables. Check the variable exists for the correct environment (Production/Preview).

  2. Redeploy to pick up the variable:

    Terminal window
    npm run deploy
  3. Access variables correctly in the Function:

    export async function onRequestPost(context) {
    const { request, env } = context;
    // Correct: access via env object
    const apiKey = env.SENDGRID_API_KEY;
    // Wrong: process.env does not work in Cloudflare Functions
    // const apiKey = process.env.SENDGRID_API_KEY; // undefined
    }

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:

PriorityIssueFixImpact
1Unoptimized imagesConvert to WebP, add width/height, use loading="lazy"High
2Render-blocking CSSInline critical CSS in <head>, defer non-criticalHigh
3Render-blocking JSAdd defer or async to <script> tagsHigh
4Third-party scriptsRemove analytics/trackers or delay loadingMedium
5Large DOMRemove unnecessary HTML elementsMedium
6Missing text compressionCloudflare Brotli (enabled by default)Low
Terminal window
# After each fix, re-run Lighthouse to measure improvement
npx lighthouse http://localhost:4173/ \
--only-categories=performance \
--output=json \
--output-path=./lighthouse-after-fix.json \
--chrome-flags="--headless"
# Extract score
cat lighthouse-after-fix.json | jq '.categories.performance.score * 100 | floor'

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.

ScriptActionHow
Google AnalyticsReplace with Cloudflare Web AnalyticsRemove GA script, enable in Cloudflare Dashboard
Facebook PixelRemove or delayRemove script tag entirely for static brochure sites
HotJar / FullStoryRemoveNot needed for static sites
Chat widgetDelay until interactionLoad script on first user interaction
reCAPTCHAReplace with Cloudflare TurnstileLighter, 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>

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:

  1. Check current DNS resolution:

    Terminal window
    # Check authoritative DNS
    dig yoursite.com +trace
    # Check from Google's DNS
    dig @8.8.8.8 yoursite.com
    # Check from Cloudflare's DNS
    dig @1.1.1.1 yoursite.com
  2. Flush local DNS cache:

    Terminal window
    # macOS
    sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
    # Windows
    ipconfig /flushdns
  3. 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

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:

Terminal window
# Find the process using port 8788
lsof -i :8788
# Kill it
kill -9 $(lsof -ti:8788)
# Or use a different port
npx wrangler pages dev public --port 8789

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):

  1. List recent deployments:

    Terminal window
    npx wrangler pages deployment list --project-name your-project-name
  2. Find the last known good deployment ID — look for the deployment before the broken one.

  3. Rollback via Cloudflare Dashboard (fastest): Pages > [Project] > Deployments > Click on last good deployment > “Rollback to this deployment”.

Alternative (redeploy last known good code):

Terminal window
# If you have the last working version in git:
git log --oneline -10
git checkout <last-good-commit>
npm run deploy
# Return to latest:
git checkout main

Critical Patients cannot submit inquiries.

  1. 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
  2. Check Functions logs:

    Terminal window
    npx wrangler pages deployment tail --project-name your-project-name
  3. If SendGrid key expired: Log into SendGrid, generate new API key. Update in Cloudflare Dashboard > Pages > Settings > Environment Variables. Redeploy: npm run deploy.

  4. If Functions missing (404 on /api/submit-form):

    Terminal window
    ls functions/api/submit-form.js
    npm run deploy
  5. Temporary workaround while fixing: Add a mailto: link or phone number prominently on the page so patients can still reach you.


  1. Rollback immediately: Cloudflare Dashboard > Pages > [Project] > Deployments. Click the previous correct deployment. Click “Rollback to this deployment”.

  2. Verify you are deploying the right project:

    Terminal window
    pwd
    cat wrangler.toml | grep name
    git branch --show-current
    npx wrangler pages deploy public --project-name correct-project-name

Terminal window
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}'

After purging: Verify the fix by hard-refreshing the browser (Cmd+Shift+R / Ctrl+Shift+R) or testing in an incognito window.


Terminal window
# Check if site is up
curl -s -o /dev/null -w "%{http_code}" https://yoursite.com/
# Check DNS resolution
dig yoursite.com A +short
# Check SSL certificate
openssl s_client -connect yoursite.com:443 -servername yoursite.com < /dev/null 2>/dev/null | openssl x509 -noout -dates
# Check security headers
curl -s -D - https://yoursite.com/ -o /dev/null | head -30
# Check Cloudflare status
curl -s https://www.cloudflarestatus.com/api/v2/summary.json | jq '.status.description'
# Check wrangler auth
npx wrangler whoami
# List deployments
npx wrangler pages deployment list --project-name your-project-name
# View Functions logs
npx wrangler pages deployment tail --project-name your-project-name
# Test form endpoint
curl -X POST https://yoursite.com/api/submit-form \
-F "name=Test" -F "email=test@test.com"
# Find process on port
lsof -i :8788
# Kill process on port
kill -9 $(lsof -ti:8788)