Redirect <project>.pages.dev to the custom domains
Each Pages project also answers on its pages.dev address: duplicate content for search engines, and stray rows in GoatCounter. That host is outside the zone, so it takes an account-level Bulk Redirect list and ruleset, and two more token permissions. Preview subdomains stay reachable. The apex record is now named by the zone name rather than "@".
a658c96 parent: 2f29c99 modified
infra/README.md +3 -2 | @@ -15,8 +15,9 @@ on. | ||
| 15 | 15 | |
| 16 | 16 | Create a token at https://dash.cloudflare.com/profile/api-tokens (Custom |
| 17 | 17 | Token, scoped to the `mykiwi.blog` zone) with `Zone:DNS:Edit`, |
| 18 | -`Zone:Zone:Read`, `Zone:Dynamic Redirect:Edit`, and | |
| 19 | -`Account:Cloudflare Pages:Edit`, then: | |
| 18 | +`Zone:Zone:Read`, `Zone:Dynamic Redirect:Edit`, `Account:Cloudflare Pages:Edit`, | |
| 19 | +`Account:Account Filter Lists:Edit`, and `Account:Account Rulesets:Edit` (the | |
| 20 | +last two for the `*.pages.dev` bulk redirects), then: | |
| 20 | 21 | |
| 21 | 22 | ```sh |
| 22 | 23 | export CLOUDFLARE_API_TOKEN=... |
| @@ -15,8 +15,9 @@ on. | |||
| 15 | 15 | ||
| 16 | Create a token at https://dash.cloudflare.com/profile/api-tokens (Custom | 16 | Create a token at https://dash.cloudflare.com/profile/api-tokens (Custom |
| 17 | Token, scoped to the `mykiwi.blog` zone) with `Zone:DNS:Edit`, | 17 | Token, scoped to the `mykiwi.blog` zone) with `Zone:DNS:Edit`, |
| 18 | -`Zone:Zone:Read`, `Zone:Dynamic Redirect:Edit`, and | 18 | +`Zone:Zone:Read`, `Zone:Dynamic Redirect:Edit`, `Account:Cloudflare Pages:Edit`, |
| 19 | -`Account:Cloudflare Pages:Edit`, then: | 19 | +`Account:Account Filter Lists:Edit`, and `Account:Account Rulesets:Edit` (the |
| 20 | +last two for the `*.pages.dev` bulk redirects), then: | ||
| 20 | 21 | ||
| 21 | ```sh | 22 | ```sh |
| 22 | export CLOUDFLARE_API_TOKEN=... | 23 | export CLOUDFLARE_API_TOKEN=... |
modified
infra/main.tf +59 -1 | @@ -45,7 +45,7 @@ resource "cloudflare_record" "site" { | ||
| 45 | 45 | for_each = local.languages |
| 46 | 46 | |
| 47 | 47 | zone_id = data.cloudflare_zone.this.id |
| 48 | - name = each.key == "en" ? "@" : each.key | |
| 48 | + name = each.key == "en" ? var.zone_name : each.key | |
| 49 | 49 | type = "CNAME" |
| 50 | 50 | content = cloudflare_pages_project.site[each.key].subdomain |
| 51 | 51 | proxied = true |
| @@ -99,3 +99,61 @@ resource "cloudflare_ruleset" "www_redirect" { | ||
| 99 | 99 | } |
| 100 | 100 | } |
| 101 | 101 | } |
| 102 | + | |
| 103 | +# <project>.pages.dev -> the custom domain. Every Pages project also answers | |
| 104 | +# on its pages.dev address, which serves the same site under a second host | |
| 105 | +# (duplicate content for search engines, stray rows in GoatCounter since the | |
| 106 | +# host is part of the counted path). That host isn't in this zone, so zone | |
| 107 | +# rules can't see it - Bulk Redirects, an account-level list plus the | |
| 108 | +# ruleset below, can. Subdomains aren't included, so the per-deploy preview | |
| 109 | +# URLs (<hash>.<project>.pages.dev) stay reachable for checking a deploy. | |
| 110 | +resource "cloudflare_list" "pages_dev_redirects" { | |
| 111 | + account_id = var.cloudflare_account_id | |
| 112 | + name = "pages_dev_redirects" | |
| 113 | + description = "<project>.pages.dev -> custom domain, path preserved" | |
| 114 | + kind = "redirect" | |
| 115 | + | |
| 116 | + dynamic "item" { | |
| 117 | + for_each = local.languages | |
| 118 | + | |
| 119 | + content { | |
| 120 | + value { | |
| 121 | + redirect { | |
| 122 | + source_url = "${cloudflare_pages_project.site[item.key].subdomain}/" | |
| 123 | + target_url = "https://${item.value.hostname}/" | |
| 124 | + status_code = 301 | |
| 125 | + include_subdomains = "disabled" | |
| 126 | + subpath_matching = "enabled" | |
| 127 | + preserve_path_suffix = "enabled" | |
| 128 | + preserve_query_string = "enabled" | |
| 129 | + } | |
| 130 | + } | |
| 131 | + } | |
| 132 | + } | |
| 133 | +} | |
| 134 | + | |
| 135 | +# Bulk Redirects need an account-level entrypoint ruleset in the | |
| 136 | +# http_request_redirect phase to actually apply the list. There can only be | |
| 137 | +# one per account and phase, so this fails to create if one already exists | |
| 138 | +# (import it instead). | |
| 139 | +resource "cloudflare_ruleset" "pages_dev_redirect" { | |
| 140 | + account_id = var.cloudflare_account_id | |
| 141 | + name = "pages.dev to custom domain redirects" | |
| 142 | + description = "Applies the ${cloudflare_list.pages_dev_redirects.name} list" | |
| 143 | + kind = "root" | |
| 144 | + phase = "http_request_redirect" | |
| 145 | + | |
| 146 | + rules { | |
| 147 | + ref = "pages_dev_redirect" | |
| 148 | + description = "Redirect <project>.pages.dev to its custom domain" | |
| 149 | + expression = format("http.request.full_uri in $%s", cloudflare_list.pages_dev_redirects.name) | |
| 150 | + action = "redirect" | |
| 151 | + | |
| 152 | + action_parameters { | |
| 153 | + from_list { | |
| 154 | + name = cloudflare_list.pages_dev_redirects.name | |
| 155 | + key = "http.request.full_uri" | |
| 156 | + } | |
| 157 | + } | |
| 158 | + } | |
| 159 | +} | |
| @@ -45,7 +45,7 @@ resource "cloudflare_record" "site" { | |||
| 45 | for_each = local.languages | 45 | for_each = local.languages |
| 46 | 46 | ||
| 47 | zone_id = data.cloudflare_zone.this.id | 47 | zone_id = data.cloudflare_zone.this.id |
| 48 | - name = each.key == "en" ? "@" : each.key | 48 | + name = each.key == "en" ? var.zone_name : each.key |
| 49 | type = "CNAME" | 49 | type = "CNAME" |
| 50 | content = cloudflare_pages_project.site[each.key].subdomain | 50 | content = cloudflare_pages_project.site[each.key].subdomain |
| 51 | proxied = true | 51 | proxied = true |
| @@ -99,3 +99,61 @@ resource "cloudflare_ruleset" "www_redirect" { | |||
| 99 | } | 99 | } |
| 100 | } | 100 | } |
| 101 | } | 101 | } |
| 102 | + | ||
| 103 | +# <project>.pages.dev -> the custom domain. Every Pages project also answers | ||
| 104 | +# on its pages.dev address, which serves the same site under a second host | ||
| 105 | +# (duplicate content for search engines, stray rows in GoatCounter since the | ||
| 106 | +# host is part of the counted path). That host isn't in this zone, so zone | ||
| 107 | +# rules can't see it - Bulk Redirects, an account-level list plus the | ||
| 108 | +# ruleset below, can. Subdomains aren't included, so the per-deploy preview | ||
| 109 | +# URLs (<hash>.<project>.pages.dev) stay reachable for checking a deploy. | ||
| 110 | +resource "cloudflare_list" "pages_dev_redirects" { | ||
| 111 | + account_id = var.cloudflare_account_id | ||
| 112 | + name = "pages_dev_redirects" | ||
| 113 | + description = "<project>.pages.dev -> custom domain, path preserved" | ||
| 114 | + kind = "redirect" | ||
| 115 | + | ||
| 116 | + dynamic "item" { | ||
| 117 | + for_each = local.languages | ||
| 118 | + | ||
| 119 | + content { | ||
| 120 | + value { | ||
| 121 | + redirect { | ||
| 122 | + source_url = "${cloudflare_pages_project.site[item.key].subdomain}/" | ||
| 123 | + target_url = "https://${item.value.hostname}/" | ||
| 124 | + status_code = 301 | ||
| 125 | + include_subdomains = "disabled" | ||
| 126 | + subpath_matching = "enabled" | ||
| 127 | + preserve_path_suffix = "enabled" | ||
| 128 | + preserve_query_string = "enabled" | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | + } | ||
| 133 | +} | ||
| 134 | + | ||
| 135 | +# Bulk Redirects need an account-level entrypoint ruleset in the | ||
| 136 | +# http_request_redirect phase to actually apply the list. There can only be | ||
| 137 | +# one per account and phase, so this fails to create if one already exists | ||
| 138 | +# (import it instead). | ||
| 139 | +resource "cloudflare_ruleset" "pages_dev_redirect" { | ||
| 140 | + account_id = var.cloudflare_account_id | ||
| 141 | + name = "pages.dev to custom domain redirects" | ||
| 142 | + description = "Applies the ${cloudflare_list.pages_dev_redirects.name} list" | ||
| 143 | + kind = "root" | ||
| 144 | + phase = "http_request_redirect" | ||
| 145 | + | ||
| 146 | + rules { | ||
| 147 | + ref = "pages_dev_redirect" | ||
| 148 | + description = "Redirect <project>.pages.dev to its custom domain" | ||
| 149 | + expression = format("http.request.full_uri in $%s", cloudflare_list.pages_dev_redirects.name) | ||
| 150 | + action = "redirect" | ||
| 151 | + | ||
| 152 | + action_parameters { | ||
| 153 | + from_list { | ||
| 154 | + name = cloudflare_list.pages_dev_redirects.name | ||
| 155 | + key = "http.request.full_uri" | ||
| 156 | + } | ||
| 157 | + } | ||
| 158 | + } | ||
| 159 | +} | ||