Cloudflare Pages serves a custom hostname only after the project knows about it. Add the domain to the project first, then publish the DNS record, or let Cloudflare publish it for you. Done the other way round, a hand-made CNAME to <project>.pages.dev fails with a 522 error, according to Cloudflare’s documentation.

This site is a Pages project named and-guide with its apex attached as a custom domain, so the checks below use its real output.

Which path your hostname takes

Hostname Requirement Who creates the DNS record
Apex, such as example.com The domain must be a zone in the same Cloudflare account as the project, with its nameservers pointed at Cloudflare Cloudflare, once you add the domain in Pages
Subdomain of a zone on Cloudflare None beyond the zone itself Cloudflare, after you confirm the record
Subdomain with DNS hosted elsewhere The zone doesn’t have to be on Cloudflare You: a CNAME from the subdomain to <project>.pages.dev
Wildcard, such as *.example.com Not supported as a Pages custom domain Not applicable

Cloudflare’s known-issues list adds two blockers: you can’t add a hostname that already has a Worker route or a Cloudflare Access policy on it. The custom-domains page adds that an Enterprise zone must release its zone hold first.

Attach the domain, then publish DNS

  1. Open Workers & Pages, select the project, then Custom domains and Set up a domain.

  2. Enter the exact hostname, and check the project name before you confirm. A similarly named project in another account is a different target.

  3. If the zone is on Cloudflare, accept the record Cloudflare offers. If DNS is hosted elsewhere, create the CNAME at that provider:

    Type:    CNAME
    Name:    shop
    Content: <project>.pages.dev
  4. Wait for the domain to show as active, then run the three checks below.

If Cloudflare Access or a Worker sits in front of the hostname, let requests to http://<hostname>/.well-known/acme-challenge/* through. Cloudflare lists blocked validation requests as a reason custom domains fail to verify.

Check 1: the custom domain answers like pages.dev

Live capture, 26 September 2026. Our apex and the project’s pages.dev hostname, both asked through 1.1.1.1:

$ dig +nocmd @1.1.1.1 and.guide A +noall +answer
and.guide.		300	IN	A	172.66.47.12
and.guide.		300	IN	A	172.66.44.244
$ dig +nocmd @1.1.1.1 and-guide.pages.dev A +noall +answer
and-guide.pages.dev.	300	IN	A	172.66.44.244
and-guide.pages.dev.	300	IN	A	172.66.47.12
$ dig +nocmd @1.1.1.1 and.guide AAAA +noall +answer
and.guide.		300	IN	AAAA	2606:4700:310c::ac42:2f0c
and.guide.		300	IN	AAAA	2606:4700:310c::ac42:2cf4
$ dig +nocmd @1.1.1.1 and-guide.pages.dev AAAA +noall +answer
and-guide.pages.dev.	300	IN	AAAA	2606:4700:310c::ac42:2cf4
and-guide.pages.dev.	300	IN	AAAA	2606:4700:310c::ac42:2f0c

The same two IPv4 and two IPv6 addresses, in a different order. The record Pages created at our apex is a CNAME that Cloudflare flattens, so the apex answers with the project’s own addresses. That makes a quick test: in a setup like ours, a custom hostname that returns the same set as <project>.pages.dev is sending traffic where Pages expects it.

Our www host, which is not attached to Pages, returns a different Cloudflare pair. Cloudflare DNS records: proxied vs DNS only shows that capture and explains why a proxied record never reveals its CNAME target.

Check 2: the same deployment on both hostnames

Live capture, 26 September 2026. Response headers from both names, filtered to the lines worth comparing:

$ curl -sI https://and.guide/ | grep -iE "^(HTTP|server|cf-ray|content-type|cache-control|x-robots-tag)"
HTTP/2 200 
content-type: text/html; charset=utf-8
cache-control: public, max-age=0, must-revalidate
server: cloudflare
cf-ray: a41315028b9a9bd9-ICN
$ curl -sI https://and-guide.pages.dev/ | grep -iE "^(HTTP|server|cf-ray|content-type|cache-control|x-robots-tag)"
HTTP/2 200 
content-type: text/html; charset=utf-8
cache-control: public, max-age=0, must-revalidate
server: cloudflare
cf-ray: a4131502ec27a7d1-ICN

Everything matches except the per-request cf-ray ID, and neither response carries an X-Robots-Tag header. Attaching a custom domain doesn’t retire the pages.dev address: it keeps serving a complete copy of the site that search engines are free to index.

Deal with the pages.dev copy

Cloudflare adds X-Robots-Tag: noindex to preview deployments by default. Its documentation says nothing similar about the production pages.dev hostname, and our capture shows no such header there. Cloudflare documents one fix for each kind of hostname:

  • Production pages.dev: Bulk Redirects. A list entry with source <project>.pages.dev, target https://example.com, status 301, and Preserve query string, Subpath matching, and Preserve path suffix enabled. Cloudflare’s example also enables Include subdomains, which by definition matches every hostname under <project>.pages.dev, including preview and branch aliases. Leave it off if you still want to open previews.
  • Preview hostnames: Cloudflare Access, so only your team can open them.

Until the redirect is in place, absolute canonical URLs at least tell search engines which host you prefer. That is what and.guide relies on today.

Live capture, 26 September 2026. The canonical tag served on the pages.dev copy:

$ curl -s https://and-guide.pages.dev/guides/ | grep -io "<link rel=\"canonical\"[^>]*>"
<link rel="canonical" href="https://and.guide/guides/">

A canonical tag is a hint to crawlers, not an access control. The redirect is the complete fix.

Check 3: the certificate belongs to the hostname

Live capture, 26 September 2026. Leaf certificates for the custom apex, the project’s pages.dev name, and our www host, read with OpenSSL 3.6.3:

$ openssl s_client -connect and.guide:443 -servername and.guide </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject -dates -ext subjectAltName
issuer=C=US, O=Google Trust Services, CN=WE1
subject=CN=and.guide
notBefore=Aug  8 05:34:39 2026 GMT
notAfter=Nov  6 06:34:36 2026 GMT
X509v3 Subject Alternative Name: 
    DNS:and.guide
$ openssl s_client -connect and-guide.pages.dev:443 -servername and-guide.pages.dev </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject -dates -ext subjectAltName
issuer=C=US, O=Let's Encrypt, CN=YE1
subject=CN=and-guide.pages.dev
notBefore=Aug  8 08:12:43 2026 GMT
notAfter=Nov  6 08:12:42 2026 GMT
X509v3 Subject Alternative Name: 
    DNS:*.and-guide.pages.dev, DNS:and-guide.pages.dev
$ openssl s_client -connect www.and.guide:443 -servername www.and.guide </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject -dates -ext subjectAltName
issuer=C=US, O=Let's Encrypt, CN=YE2
subject=CN=and.guide
notBefore=Sep  6 14:25:26 2026 GMT
notAfter=Dec  5 14:25:25 2026 GMT
X509v3 Subject Alternative Name: 
    DNS:*.and.guide, DNS:and.guide

What to read from it:

  • The custom domain has a certificate of its own. and.guide presents a Google Trust Services certificate that names exactly one host.
  • The pages.dev certificate is a Let’s Encrypt wildcard for *.and-guide.pages.dev, which is how preview aliases get HTTPS.
  • www.and.guide presents the zone’s Universal SSL certificate (Let’s Encrypt, covering *.and.guide and and.guide). Same zone, different certificate, different CA.
  • All three are valid for about 90 days.

Cloudflare picks the issuing CA. Its certificate-authority reference lists Let’s Encrypt, Google Trust Services, and SSL.com for both Universal SSL and SSL for SaaS certificates. The Pages known-issues page notes that Advanced Certificates can’t be used with Pages because of how Cloudflare for SaaS prioritizes certificates. That points to Pages custom domains getting their certificates through the SaaS path, which would explain why our apex carries its own certificate instead of the zone’s Universal SSL one.

Plan CAA for all three CAs

We publish no CAA records for and.guide (a CAA query returns an empty answer), so any CA may issue. If you do publish CAA, Cloudflare’s Pages documentation lists what to allow, as both issue and issuewild:

example.com.  300  IN  CAA  0 issue "letsencrypt.org"
example.com.  300  IN  CAA  0 issue "pki.goog; cansignhttpexchanges=yes"
example.com.  300  IN  CAA  0 issue "ssl.com"
example.com.  300  IN  CAA  0 issuewild "letsencrypt.org"
example.com.  300  IN  CAA  0 issuewild "pki.goog; cansignhttpexchanges=yes"
example.com.  300  IN  CAA  0 issuewild "ssl.com"

A policy that allowed only Let’s Encrypt would have blocked issuance of the Google Trust Services certificate our apex uses today. CAA records and Let’s Encrypt explains how CAA lookups climb the name tree, and inspecting a TLS certificate from the command line covers more openssl checks.

Redirect www to the apex

The Pages _redirects file handles paths only; Cloudflare lists domain-level redirects as unsupported there. The documented approach uses Bulk Redirects:

  1. Create a Bulk Redirect list entry: source www.example.com, target https://example.com, status 301, with Preserve query string, Subpath matching, Preserve path suffix, and Include subdomains enabled.
  2. Create a proxied DNS record for www: type A, name www, IPv4 address 192.0.2.1, proxy status Proxied. The address is a documentation placeholder; the record only has to exist and be proxied so the redirect can act on requests.
  3. Create a Bulk Redirect rule that uses the list.

Live capture, 26 September 2026. Checking that the path and query string survive our own redirect:

$ curl -sI "https://www.and.guide/guides/?ref=test" | grep -iE "^(HTTP|location)"
HTTP/2 301 
location: https://and.guide/guides/?ref=test

A permanent redirect that keeps both the path and the query. If your location header loses the path, Subpath matching or Preserve path suffix is off; if it loses ?ref=test, Preserve query string is off. For choosing which host should be canonical in the first place, see www vs apex canonical redirects.

Remove a custom domain in the right order

Cloudflare’s documented order is to delete the CNAME from the zone’s DNS records first, then remove the domain under Custom domains. Doing only the second step leaves a record aimed at your project for a hostname that Pages no longer serves.

Symptoms and likely causes

Symptom Likely cause Check
522 on the custom hostname The CNAME was created without adding the domain in Pages Add it under Custom domains
The domain stays in verification CAA excludes Cloudflare’s CAs, or Access or a Worker blocks /.well-known/acme-challenge/ dig example.com CAA +short; your Access policies and Worker routes
The domain can’t be added A Worker route or Access policy on the hostname, a wildcard, or an apex that isn’t a zone in this account The known-issues list; which account holds the zone
The custom hostname and pages.dev return different addresses The record points somewhere else Compare dig output for both names
pages.dev still ranks in search results The production copy is reachable and indexable A Bulk Redirect, or canonical tags as a stopgap