Tomas Pustelnik published How and why I’ve migrated from Netlify to Cloudflare on 12 May 2021. He updated it on 6 November 2021 when Pages gained a _headers file. This is his own post, not a Cloudflare page.
The site is Eleventy. The repo is Pustelto/personal_web. Netlify had hosted it. He liked the free plan, the GitHub deploys, and the extra services. Then Cloudflare launched Pages. He wanted to know if a host change would show up in load time.
If you are looking at the same route, start with the Netlify → Cloudflare Pages playbook. Cloudflare’s Migrating from Netlify to Pages guide covers the Git app, the build command, and the publish directory. Tomas is the field report for everything that sat outside the repo.
Why he left
He still calls Netlify a great host. The reason to try Pages was performance. Cloudflare already had a reputation for CDN and networking. He wanted a before-and-after on his own pages.
He does not publish a Netlify bill, a region, or a Lighthouse JSON. Speed is the reason he gives.
How he moved
Connecting the GitHub repo was the easy part. He picked a branch, set the build command, and Pages built the site. Preview deploys came with it.
The site was not done. He was using more of the Netlify product than the Git hook. Build plugins and snippet injection live in the Netlify project, not in the Markdown. Cloudflare’s migrate guide is blunt about that: netlify.toml extras do not come across, and you delete the file after you rewrite the pieces Pages can keep.
His list was five items: the Critical CSS plugin, production-only analytics, the domain, custom headers, and redirects.
The plugin inlined Critical CSS so the HTML could paint before the rest of the stylesheet arrived. Pages has no plugin marketplace like Netlify’s. The plugins are open source, so he copied the GitHub source into a Node script and ran it from the build. That took a few minutes. The public repo still has a critical.js file at the root.
Plausible was the analytics snippet. On Netlify he injected it at build time so local and preview HTML did not fire events. Pages has no snippet injector. He borrowed the isDevelopment flag from Google’s eleventy-high-performance-blog template and gated the script in Eleventy data. Another copy-paste.
The domain sat at Vercel. Vercel sold him the name, but it is not the registrar, so nameserver edits go through support. Their note for free accounts was 3 to 5 days. His change landed the next day. Once Cloudflare saw the nameservers, he could attach the custom domain, point a Worker at it, and set the redirects.
Headers were the slow part. In May 2021 Pages had no _headers file. He wrote a Worker that fetched the page and patched the response:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const reqUrl = new URL(request.url)
let response = await fetch(request)
response = new Response(response.body, response)
if (reqUrl.pathname.endsWith('.avif')) {
response.headers.set('Content-Type', 'image/avif')
response.headers.set('Content-Disposition', 'inline')
}
response.headers.set('Permissions-Policy', 'interest-cohort=()')
response.headers.set('X-Frame-Options', 'sameorigin')
response.headers.set('strict-transport-security', 'max-age=31536000')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
return response
}
A Worker route needs a custom domain. He could not attach it to the default *.pages.dev URL. No custom domain, no header Worker.
On 6 November 2021 he came back and added the Pages _headers path. In Eleventy that is a passthrough copy into the build output:
config.addPassthroughCopy('src/_headers')
His checked-in file sets security headers for /*, forces AVIF to image/avif, and puts X-Robots-Tag: noindex on https://:project.pages.dev/*. That last rule is in Cloudflare’s own headers docs. I fetched pustelto.com in October 2026. The live response still sends those headers, and the host is still Cloudflare.
Redirects were two hops. Netlify had sent www to the apex when he set DNS. The second hop was *.pustelto.cz to https://pustelto.com. He moved that second zone to Cloudflare and used Page Rules. The free plan gave him 3 rules, enough for this site. He says a bigger redirect table can blow that limit, or you buy more rules. Today’s Pages _redirects file is the in-repo option, with a 2,000 static plus 100 dynamic cap. He did not use that file in 2021.
What he gained
He measured on WebPageTest, mobile emulation of a Moto G (4th gen) on fast 3G. Those are his numbers.
First Contentful Paint and Largest Contentful Paint were almost 400 ms faster on Cloudflare. Visual complete was 1.1 s against 2 s on Netlify. Across pages he called it more than 300 ms. The Cloudflare payload was a bit smaller. He credits Brotli.
The Netlify waterfall fetched an image from another hostname, so the browser opened a second TLS session. That is the extra wait he points at.
The whole rewrite took a few hours. He does not publish a bill. He says the basic Pages setup was free, and that Workers plus extra page routes can push a more complex site onto a paid Cloudflare plan.
What broke or got skipped
The Git deploy was not feature parity. Plugins, snippets, headers, and redirects all needed a second pass. Cloudflare’s migrate guide still lists the same gaps: _redirects can come across with fewer conditions, netlify.toml rules need a rewrite, forms lose data-netlify, and Functions use a different handler. He does not describe Forms or Functions. An Eleventy blog often has none.
He also does not describe exporting Netlify form submissions, Identity users, deploy history, or analytics. Those stay on Netlify unless you copy them.
The May 2021 Worker path is dated. _headers landed six months later. If you copy his Worker today, you are solving a problem Pages already handles for static assets. _headers still does not apply to Pages Functions responses. A Function route still needs headers in the Worker code.
Page Rules are a zone feature, not a file in the repo. Three free rules was enough for pustelto.cz. Count yours before you assume the same.
He does not describe a failed build or a rollback to Netlify. The site is still on Cloudflare.
What I would copy
Inventory the Netlify dashboard before you connect the GitHub app. Plugins, snippets, env vars, headers, and redirects are the portable list. The repo is only the HTML.
Turn each plugin into a script you can run in any CI. Tomas’s Critical CSS move is that pattern. If the plugin is closed, you do not have that shortcut.
Put analytics in the templates, gated on an environment flag. Snippet injection will not follow you.
Use _headers and _redirects in the publish directory so the next host can read them. Keep Page Rules for zone-level hops like an old domain, and count the free slots.
Associate the custom domain in Pages before you point DNS. Tomas needed that for the Worker. Cloudflare’s custom domains docs still want the association first.
Measure with the same WebPageTest recipe on both hosts. His 400 ms is a Moto G on fast 3G in 2021, not a promise.
Run the Netlify → Cloudflare Pages playbook. Use Tomas’s post for the dashboard features that never made it into the repo.