Skip to content
← Blog18 Oct 2026 · 14 min read

Moving a business site from Vercel to Cloudflare Pages, with the domain at GoDaddy

A step-by-step guide to hosting a small business's static site for free on Cloudflare Pages: why Vercel's free plan didn't fit, moving a GoDaddy domain's DNS to Cloudflare without moving the domain, the www redirect, a staging site on its own subdomain kept private with Cloudflare Access, and the problems I hit on the way. Written after doing it for my family's catalogue site in October.

CloudflareHostingFreelance

In October I built a catalogue website for FI Woodart, my family's wood-art business. I put it on Vercel first, because that's what I always reach for. Then the family bought a domain at GoDaddy, and before pointing it anywhere I reread Vercel's terms. The free Hobby plan is "restricted to non-commercial personal use only", and Vercel's fair use guidelines list "advertising the sale of a product or service" as commercial use. A catalogue of things for sale is exactly that. Staying meant Vercel's Pro plan, at about $20 a month, for a static site with no server code.

Cloudflare Pages allows commercial sites on its free plan, so I moved. The switch itself took one evening. This guide covers every step in order, including the ones that confused me. It assumes a static site in a GitHub repository (mine is built with Astro) and a domain registered at GoDaddy. Cloudflare and GoDaddy change their screens often, so the menu names may have moved by the time you read this.

What you end up with

  • The live site at your domain, built by Cloudflare Pages from the main branch.
  • www redirecting to the bare domain.
  • A staging site at staging.yourdomain, built from a staging branch, that only people you name can open, and that search engines never see.
  • The domain still registered and renewed at GoDaddy, with its DNS run by Cloudflare.

All of it is free. Cloudflare Pages' free plan allows 500 builds a month, one at a time, and states no bandwidth limit for a static site. Cloudflare Access, which keeps staging private, comes with 50 free seats on every account. The only bill is the domain renewal at GoDaddy.

The idea that makes the rest make sense

Your domain has a registrar and a DNS host, and they don't have to be the same company. The registrar is where you bought the name and where you renew it. The DNS host answers the question "where does this name point?". At first GoDaddy is both.

Cloudflare Pages can only serve the bare domain (fiwoodart.co.in rather than www.fiwoodart.co.in) if Cloudflare is the DNS host. So you keep GoDaddy as the registrar and hand only the DNS to Cloudflare, by changing the domain's nameservers. You still renew at GoDaddy. Turn on auto-renew there. After the change, the DNS records in your GoDaddy account stop mattering, because nothing reads them any more.

You can also move the registration itself to Cloudflare later, but a domain can't change registrar in its first 60 days, and you don't need to.

Before you touch DNS: prepare the code

Do this first, while the old host is still serving. If your site behaves the same on both hosts, you can switch the domain without a broken moment.

Tell production apart from everything else. Cloudflare Pages sets CF_PAGES_BRANCH on every build. I treat only a main build as production, and anything else as staging that must not be indexed:

// src/lib/env.ts
const branch = process.env.CF_PAGES_BRANCH;

export const isProduction = branch === "main";
export const isStaging = !isProduction;

Without this, nothing distinguishes the live build from a preview, and the live site can ship hidden from Google with a staging banner on it. That is exactly what happened to me for two minutes, and it's further down.

Give each build the right address. Canonical tags and the sitemap need absolute URLs. Production should use your domain, and previews their own address, which Cloudflare provides as CF_PAGES_URL:

// astro.config.mjs
const env = process.env;
const host = env.CF_PAGES_BRANCH === "main" ? env.SITE_URL : env.CF_PAGES_URL;

export default defineConfig({
  site: host || "http://localhost:4321",
  build: { format: "file" },
});

SITE_URL is a variable you set in Cloudflare, in step 1.

Check what happens to /about. On Vercel I had clean URLs turned on. On Pages, a site built as about/index.html answers /about with a redirect to /about/. Building pages as about.html instead (format: "file" above) makes Pages serve /about directly. If you do this, check your canonical tag. Mine was built from the page's path, which now ended in .html, so every canonical said /about.html. I strip it:

const canonical = new URL(Astro.url.pathname.replace(/(index)?\.html$/, ""), Astro.site);

Move your headers. Pages doesn't read vercel.json. Put response headers in a _headers file in the folder that gets published (public/ in Astro):

/_astro/*
  Cache-Control: public, max-age=31536000, immutable

/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

Redirects, if you have any, go in a _redirects file next to it.

Pin Node. A .node-version file at the root containing 22 tells Pages which Node to build with.

1. Create the Pages project

In the Cloudflare dashboard, go to Workers & Pages, then Create. Make sure you're on the Pages tab, because the page opens on Workers by default. Choose Connect to Git and pick your repository.

  • Framework preset: Astro (or yours). Build command npm run build, output directory dist.
  • Environment variables: SITE_URL = https://yourdomain, set for Production only, so previews never claim the real domain.

The first build gives you your-project.pages.dev. Check it before going further.

2. Build only main and staging

By default Pages builds a preview of every branch you push. I wanted two builds and no more: main for the live site and staging for testing.

Go to Settings, then Build, then Branch control. Set the production branch to main, set Preview branch to Custom branches, and include staging.

Two things caught me here. The include box is a chip input: type staging and press Enter so it turns into a chip before you save. If you just type and save, the text is thrown away. Mine silently kept a * chip, which means every branch, and feature branches went on building. And "All non-Production branches" ignores the include list completely, so don't choose it to save time.

Saving these settings doesn't start a build. To get the first staging build, push something to staging; an empty commit does it. After any push, wait a few minutes before checking whether a branch built. I once checked a minute and a half after a push, saw nothing, and wrongly decided the setting worked.

3. Add the domain to Cloudflare

In the dashboard, add your domain (it's under Domains, as onboarding a domain) and choose the Free plan. If you start from the Pages project's Custom domains tab instead, Cloudflare offers to "Begin DNS transfer". That sounds like moving the registration, but it only means adding the domain to Cloudflare's DNS. It leads to the same place.

Cloudflare scans GoDaddy's existing records and copies them in. The quick scan isn't guaranteed to find everything, so compare it against GoDaddy's DNS page. For a domain that only ever had GoDaddy's defaults, I deleted all four imported records:

  • two A records on the bare domain, which point at GoDaddy's parking page
  • a CNAME called _domainconnect, a GoDaddy helper
  • a CNAME for www pointing at the bare domain

Pages creates the records it needs in step 6, and stale ones get in the way. Keep any MX and TXT records if the domain has email. I had none. Deleting records here only changes Cloudflare's copy, so it's safe; GoDaddy's originals don't move.

Cloudflare then shows you two nameservers assigned to your domain. Copy them exactly.

4. Change the nameservers at GoDaddy

In GoDaddy, open the domain's DNS settings. It took me two tries to find them: the first page I landed on was GoDaddy's "GoConnect" shortcut page, which has no nameserver setting. The route that worked was the domain's Manage DNS card, which has tabs for DNS Records, Forwarding, Nameservers, Hostnames and DNSSEC.

  1. On the DNSSEC tab, make sure it's off. Cloudflare says to turn it off before changing nameservers, or the domain can stop resolving. You can turn it back on through Cloudflare once the move is done.
  2. On the Forwarding tab, make sure nothing is forwarded.
  3. On the Nameservers tab, choose Change Nameservers, then I'll use my own nameservers.
  4. Replace GoDaddy's two nameservers with Cloudflare's two, save, and accept the warning. GoDaddy may ask for a verification code.

GoDaddy says most changes take effect within an hour and can take up to 48.

5. Wait for Cloudflare to show "Active"

Mine went Active in about eight minutes. Before that, the checks disagreed, which looked alarming but wasn't. The registry had the new nameservers straight away, while public resolvers kept giving the old answer until their cached copy expired, an hour later at most. To see what a public resolver currently thinks:

nslookup -type=NS yourdomain 1.1.1.1

Cloudflare also emails you when the domain becomes active.

6. Connect the domain and www to Pages

In the Pages project, go to Custom domains, choose Set up a custom domain, enter the bare domain, and activate it. Cloudflare creates the DNS record itself: a CNAME from the bare domain to your-project.pages.dev. A CNAME isn't normally allowed at the bare domain, but Cloudflare flattens it, so it works. Then do the same for www.

Doing it twice felt redundant to me at first. It isn't: they are two different names, and each needs its own record and its own certificate. Both of mine were active within about six minutes, certificates included. Don't create these CNAME records by hand before adding the domains in Pages. Cloudflare warns that a record pointing at Pages without the domain being added there first gives a 522 error.

Both records should be proxied, the orange cloud. Steps 7 and 9 depend on it.

7. Redirect www to the bare domain

In your domain's Rules, create a rule from the template Redirect from WWW to root. It matches https://www.*, sends it to https://${1}, with a 301 and Preserve query string ticked. Cloudflare's version is the same. Deploy it.

Mine showed as active, and www still didn't redirect on my laptop for a while. The rule was fine. My internet provider's resolver still had GoDaddy's old address for www cached, so my requests never reached Cloudflare. Flushing the computer's DNS cache doesn't help when the stale answer comes from further up. It sorted itself out within the hour, and a phone on mobile data showed the redirect working sooner. To test the rule regardless of your resolver, send the request straight to Cloudflare with curl --resolve.

8. Put the real site live

The moment the domain connected, it served whatever was on main. For me that was a build from before the code changes above, so the live domain showed a "Staging · local" ribbon, told search engines not to index it, and had a canonical tag pointing at localhost. Merging staging into main fixed all of it two minutes later. It was a good accident: an unready site that hides itself from Google is the safe way round.

Once the production build finishes, check the live site from a terminal:

curl -sI https://yourdomain/about         # 200, no redirect
curl -sI https://www.yourdomain/          # 301 to https://yourdomain/
curl -s  https://yourdomain/robots.txt    # Allow, with your sitemap's address

Then view the page source and confirm the canonical tag uses your domain, with no .html on the end.

9. Give staging its own subdomain

Pages already serves the staging branch at staging.your-project.pages.dev. A subdomain of your own is easier to remember and send to people. Cloudflare calls this a custom branch alias:

  1. In the Pages project, add staging.yourdomain as a custom domain, as in step 6. For now it serves the live site.
  2. In DNS, edit the new staging record and change its target from your-project.pages.dev to staging.your-project.pages.dev.
  3. Keep it proxied. On an unproxied record, Cloudflare quietly sends the subdomain to production instead.

10. Make staging private

Staging should be visible to you and the people checking it, not the public. Cloudflare Access puts a login in front of a hostname. The dashboard now calls this part of Cloudflare Cloudflare One, formerly Zero Trust. Setting it up asks you for a team name; mine started as a random one, which I renamed.

  1. Under Access controls, then Applications, add a Self-hosted application. Give it a name and a session length (I chose a month). The destination is the public hostname staging.yourdomain. Never enter the bare domain or * here, or you lock everyone out of the live site.
  2. Add a policy that Allows people whose Emails you list: yours and whoever is checking the site.
  3. Add an email login. My login page at first offered only a Cloudflare account sign-in, which the people checking the site didn't have. Under Integrations, then Identity providers, add One-time PIN. Now anyone on the list types their email address and gets a code.

That covers your subdomain, but staging is also reachable at its pages.dev address, which stayed public. Close it in the Pages project: Settings, then General, then Access policy, then Enable. This creates a second Access application covering all your preview addresses. By default it admits only you, the account owner, so add the same emails to its policy.

Check both from a private window. Each staging address should land on a sign-in page asking for an email. The live domain and your-project.pages.dev, which mirrors production, should load as normal.

Access already stops search engines reaching staging. I keep the site's own guard as well, because it costs nothing. Every non-production build tells crawlers to stay away, in the robots file:

// src/pages/robots.txt.ts
export const GET: APIRoute = ({ site }) => {
  const body = isProduction
    ? `User-agent: *\nAllow: /\n\nSitemap: ${new URL("sitemap-index.xml", site)}\n`
    : "User-agent: *\nDisallow: /\n";
  return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
};

It also adds <meta name="robots" content="noindex, nofollow"> and a small "Staging" ribbon to every page. Pages also sends a noindex header on preview addresses by default.

12. Tell Google about the site

In Google Search Console, add a Domain property for the bare domain. This covers www, every subdomain and both protocols in one property. Domain properties are verified by a TXT record. Because the DNS is on Cloudflare, Search Console offered to add the record itself, and verification finished without my typing anything into DNS. Leave that record in place for good.

Then submit the sitemap. A Domain property needs the full address, such as https://yourdomain/sitemap-index.xml. Use URL inspection to request indexing for your main pages if you don't want to wait for Google to find them.

If you're doing this for a client, set it up under your own Google account and add the client as an Owner, so they keep access whatever happens to you.

13. Retire the old host

Once the live site is right, disconnect the repository in Vercel's project settings, so pushes stop building there too. Then delete vercel.json and any code that checked Vercel's variables. To confirm the disconnect, check the repository: Vercel's status check stops appearing on new pushes.

Mistakes to avoid

MistakeWhat happens
Following Vercel advice to keep DNS at GoDaddyFine on Vercel, but Pages can't serve the bare domain unless Cloudflare runs the DNS
Typing into Branch control's include box without pressing EnterThe text is discarded on save, and every branch keeps building
Leaving GoDaddy's imported records in CloudflareParking records and the old www record clash with the ones Pages creates
Changing nameservers with DNSSEC still onThe domain can stop resolving
Adding the Pages CNAME by hand before adding the domain in PagesA 522 error
An unproxied staging recordThe staging subdomain quietly shows the live site
Protecting only the staging subdomain with AccessIts pages.dev address stays open to anyone until you enable the project's access policy
Pointing an Access application at the bare domain or *Your live site sits behind a login
Judging the www redirect from your own laptop right awayYou may be seeing your provider's cached answer, not Cloudflare's
Newsletter

More like this, occasionally.

Notes on building AI products. No spam, unsubscribe anytime.