Show ANAME for apex domains

This commit is contained in:
Jon Staab 2026-09-17 10:52:59 -07:00
parent 5ca8eeff51
commit 95b92a07f1
7 changed files with 45 additions and 24 deletions

View file

@ -1519,8 +1519,9 @@ space carries my branding.
Acceptance:
- Saving a domain under "Manage" shows it on the relay card with a "Pending"
badge and the CNAME record to configure, whose copy button puts the target on
the clipboard.
badge and the DNS record to configure, whose copy button puts the target on
the clipboard. A subdomain gets a CNAME; a bare domain, which DNS won't let
take one, gets an ALIAS.
- "Verify DNS record" flips the badge to "Verified" once the backend reports it.
- The relay's displayed address then switches to the custom domain.

View file

@ -931,15 +931,23 @@ test("US-101 point a custom domain at a hosted relay", async ({seed, as}) => {
const domainForm = dialog(page, "Custom domain")
await domainForm.getByPlaceholder("relay.example.com").fill("other.test")
await domainForm.getByPlaceholder("relay.example.com").fill("relay.other.test")
await domainForm.getByRole("button", {name: "Save", exact: true}).click()
await expect(page.getByRole("alert")).toContainText("Custom domain saved.")
await expect(page.getByText("other.test", {exact: true})).toBeVisible()
await expect(page.getByText("relay.other.test", {exact: true})).toBeVisible()
await expect(page.getByText("Pending", {exact: true})).toBeVisible()
await expect(page.getByText("other.test CNAME space.test")).toBeVisible()
await expect(page.getByText("relay.other.test CNAME space.test")).toBeVisible()
await page.locator('[data-tip="Copy CNAME target"]').click()
// A bare domain can't take a CNAME, so it's shown as an ALIAS at the same target
await page.getByRole("button", {name: "Manage"}).click()
await domainForm.getByPlaceholder("relay.example.com").fill("other.test")
await domainForm.getByRole("button", {name: "Save", exact: true}).click()
await expect(page.getByText("other.test", {exact: true})).toBeVisible()
await expect(page.getByText("other.test ALIAS space.test")).toBeVisible()
await page.locator('[data-tip="Copy ALIAS target"]').click()
await expect(page.getByRole("alert")).toContainText("Copied to clipboard!")
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe("space.test")

View file

@ -2,6 +2,7 @@
import type {Snippet} from "svelte"
import cx from "classnames"
import {Profiles} from "@welshman/app"
import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
@ -67,6 +68,11 @@
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
{/if}
</PrimaryNavItem>
{#if PLATFORM_RELAYS.length === 0}
<PrimaryNavItem href="/spaces" title="All Spaces" prefix="no-highlight">
<ImageIcon alt="All Spaces" src={Compass} size={8} />
</PrimaryNavItem>
{/if}
<PrimaryNavItem
title="Messages"
onclick={chatHandler}

View file

@ -1,6 +1,5 @@
<script lang="ts">
import cx from "classnames"
import Widget from "@assets/icons/widget-4.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
@ -46,11 +45,11 @@
<PrimaryNavItem title="Home" href="/home">
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" size={10} />
</PrimaryNavItem>
<Divider />
<Divider class="w-full" />
<div
bind:this={element}
onscroll={measure}
class="primary-nav__spaces"
class="primary-nav__spaces mb-2"
style:--fade-top="{Math.min(hiddenAbove, fadeSize)}px"
style:--fade-bottom="{Math.min(hiddenBelow, fadeSize)}px">
<DragList
@ -62,8 +61,5 @@
{/snippet}
</DragList>
</div>
<PrimaryNavItem href="/spaces" title="All Spaces" prefix="no-highlight">
<ImageIcon alt="All Spaces" src={Widget} size={8} />
</PrimaryNavItem>
{/each}
</div>

View file

@ -70,7 +70,11 @@
const host = $derived(relayHost(current))
const relayLabel = $derived(current.info_name || current.subdomain)
const domainVerified = $derived(flagToBool(current.custom_domain_verified, false))
const cnameTarget = $derived(canonicalRelayHost(current))
const recordTarget = $derived(canonicalRelayHost(current))
// DNS forbids a CNAME at a zone apex, so those domains point at the same
// target using their provider's ALIAS/ANAME record instead.
const isApex = $derived(current.custom_domain.split(".").length === 2)
const recordType = $derived(isApex ? "ALIAS" : "CNAME")
const isPaidPlan = $derived(current.plan_id !== "free")
// Adding, removing or verifying a custom domain moves the relay's host, so
@ -156,7 +160,7 @@
showMenu = false
}
const copyCname = () => clip(cnameTarget)
const copyTarget = () => clip(recordTarget)
// Verification runs in a backend poller; reload to pick up the result.
const verify = async () => {
@ -346,18 +350,23 @@
</div>
<div class="flex items-center gap-2 rounded-xl border border-line bg-surface px-3 py-2">
<code class="min-w-0 flex-1 break-all font-mono text-xs text-content">
{current.custom_domain} CNAME {cnameTarget}
{current.custom_domain}
{recordType}
{recordTarget}
</code>
<Button
class="button button-neutral button-sm shrink-0"
data-tip="Copy CNAME target"
onclick={copyCname}>
data-tip="Copy {recordType} target"
onclick={copyTarget}>
<Icon icon={Copy} size={4} />
</Button>
</div>
<p class="text-xs text-content-muted">
For apex domains (e.g. example.com), use an ALIAS or ANAME record instead.
</p>
{#if isApex}
<p class="text-xs text-content-muted">
A bare domain can't use a CNAME. Your provider may call this record ANAME, or offer
CNAME flattening instead.
</p>
{/if}
<Button
class="button button-neutral button-sm self-start"
onclick={verify}

View file

@ -367,7 +367,7 @@ export const relayHost = (relay: HostedRelay): string =>
? relay.custom_domain
: canonicalRelayHost(relay)
// The host a custom domain must CNAME to.
// The host a custom domain's DNS record must point at.
export const canonicalRelayHost = (relay: HostedRelay): string =>
`${relay.subdomain}.${relay.zooid_domain}`

View file

@ -1,14 +1,15 @@
<script lang="ts">
import type {Snippet} from "svelte"
interface Props {
type Props = {
children?: Snippet
class?: string
}
const {children}: Props = $props()
const {children, class: className}: Props = $props()
</script>
<div class="flex items-center gap-2 p-2 text-xs uppercase text-content-subtle">
<div class="flex items-center gap-2 p-2 text-xs uppercase text-content-subtle {className}">
<div class="h-px grow" style="background-color: var(--line)"></div>
{#if children}
<p>{@render children?.()}</p>