diff --git a/e2e/USER_STORIES.md b/e2e/USER_STORIES.md
index 7c1a537f..1f5d758f 100644
--- a/e2e/USER_STORIES.md
+++ b/e2e/USER_STORIES.md
@@ -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.
diff --git a/e2e/specs/admin.spec.ts b/e2e/specs/admin.spec.ts
index 0cf90116..a5a86463 100644
--- a/e2e/specs/admin.spec.ts
+++ b/e2e/specs/admin.spec.ts
@@ -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")
diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte
index ad966ca3..54473070 100644
--- a/src/app/components/PrimaryNav.svelte
+++ b/src/app/components/PrimaryNav.svelte
@@ -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 @@
{/if}
+ {#if PLATFORM_RELAYS.length === 0}
+
+
+
+ {/if}
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 @@
-
+
-
-
-
{/each}
diff --git a/src/app/components/hosting/RelayDetailCard.svelte b/src/app/components/hosting/RelayDetailCard.svelte
index 60f71700..df213fa7 100644
--- a/src/app/components/hosting/RelayDetailCard.svelte
+++ b/src/app/components/hosting/RelayDetailCard.svelte
@@ -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 @@