Skip to content

Commit 67f30ea

Browse files
committed
Detect cname flattening and alias support
1 parent 9f51b97 commit 67f30ea

File tree

7 files changed

+177
-131
lines changed

7 files changed

+177
-131
lines changed

apps/builder/app/builder/features/topbar/add-domain.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { Project } from "@webstudio-is/project";
1414
import { useId, useOptimistic, useRef, useState } from "react";
1515
import { TerminalIcon } from "@webstudio-is/icons";
1616
import { nativeClient } from "~/shared/trpc/trpc-client";
17+
import { extractCname } from "./cname";
1718

1819
type DomainsAddProps = {
1920
projectId: Project["id"];
@@ -38,14 +39,25 @@ export const AddDomain = ({
3839
// Will be automatically reset on action end
3940
setIsPendingOptimistic(true);
4041

41-
const domain = formData.get("domain")?.toString() ?? "";
42+
let domain = formData.get("domain")?.toString() ?? "";
4243
const validationResult = validateDomain(domain);
4344

4445
if (validationResult.success === false) {
4546
setError(validationResult.error);
4647
return;
4748
}
4849

50+
// detect provider only when root domain is specified
51+
if (extractCname(domain) === "@") {
52+
const registrar = await nativeClient.domain.findDomainRegistrar.query({
53+
domain,
54+
});
55+
// enforce www subdomain when no support for cname flattening or alias
56+
if (!registrar.cnameFlattening && !registrar.alias) {
57+
domain = `www.${domain}`;
58+
}
59+
}
60+
4961
const result = await nativeClient.domain.create.mutate({
5062
domain,
5163
projectId,

apps/builder/app/builder/features/topbar/domain-checkbox.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface DomainCheckboxProps {
2020
disabled?: boolean;
2121
}
2222

23-
const DomainCheckbox = (props: DomainCheckboxProps) => {
23+
export const DomainCheckbox = (props: DomainCheckboxProps) => {
2424
const hasProPlan = useStore($userPlanFeatures).hasProPlan;
2525
const project = useStore($project);
2626

@@ -78,5 +78,3 @@ const DomainCheckbox = (props: DomainCheckboxProps) => {
7878
</div>
7979
);
8080
};
81-
82-
export default DomainCheckbox;

apps/builder/app/builder/features/topbar/domains.tsx

Lines changed: 121 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from "@webstudio-is/icons";
2222
import { CollapsibleDomainSection } from "./collapsible-domain-section";
2323
import {
24+
Fragment,
2425
startTransition,
2526
useEffect,
2627
useOptimistic,
@@ -29,17 +30,18 @@ import {
2930
type ReactNode,
3031
} from "react";
3132
import { Entri } from "./entri";
32-
import { nativeClient } from "~/shared/trpc/trpc-client";
33+
import { nativeClient, trpcClient } from "~/shared/trpc/trpc-client";
3334
import { useStore } from "@nanostores/react";
3435
import { $publisherHost } from "~/shared/nano-states";
3536
import { extractCname } from "./cname";
3637
import { useEffectEvent } from "~/shared/hook-utils/effect-event";
37-
import DomainCheckbox from "./domain-checkbox";
38+
import { DomainCheckbox } from "./domain-checkbox";
3839
import { CopyToClipboard } from "~/builder/shared/copy-to-clipboard";
3940
import { RelativeTime } from "~/builder/shared/relative-time";
4041

4142
export type Domain = Project["domainsVirtual"][number];
42-
type DomainStatus = Project["domainsVirtual"][number]["status"];
43+
44+
type DomainStatus = Domain["status"];
4345

4446
const InputEllipsis = styled(InputField, {
4547
"&>input": {
@@ -164,6 +166,119 @@ const StatusIcon = (props: { projectDomain: Domain; isLoading: boolean }) => {
164166
);
165167
};
166168

169+
const DomainConfig = ({
170+
projectDomain,
171+
onUpdateStatus,
172+
}: {
173+
projectDomain: Domain;
174+
onUpdateStatus: () => void;
175+
}) => {
176+
const { load: findDomainRegistrar, data: registrar } =
177+
trpcClient.domain.findDomainRegistrar.useQuery();
178+
const cname = extractCname(projectDomain.domain);
179+
useEffect(() => {
180+
if (cname === "@") {
181+
findDomainRegistrar({ domain: projectDomain.domain });
182+
}
183+
}, [projectDomain.domain, cname]);
184+
const publisherHost = useStore($publisherHost);
185+
186+
const cnameRecord = {
187+
// use alias for domain root when supported to avoid conflicting
188+
// with MX, NS etc records
189+
type: cname === "@" && registrar?.alias ? "ALIAS" : "CNAME",
190+
host: cname,
191+
value: `${projectDomain.cname}.customers.${publisherHost}`,
192+
ttl: 300,
193+
} as const;
194+
195+
const txtRecord = {
196+
type: "TXT",
197+
host: cname === "@" ? "_webstudio_is" : `_webstudio_is.${cname}`,
198+
value: projectDomain.expectedTxtRecord,
199+
ttl: 300,
200+
} as const;
201+
202+
const dnsRecords = [cnameRecord, txtRecord];
203+
204+
return (
205+
<>
206+
<Text color="subtle">
207+
<strong>To verify your domain:</strong>
208+
<br />
209+
Visit the admin console of your domain registrar (the website you
210+
purchased your domain from) and create one <strong>CNAME</strong> record
211+
and one <strong>TXT</strong> record with the values shown below:
212+
</Text>
213+
214+
<Grid
215+
gap={2}
216+
css={{ gridTemplateColumns: `${theme.spacing[18]} 1fr 1fr` }}
217+
>
218+
<Text color="subtle" variant="titles">
219+
TYPE
220+
</Text>
221+
<Text color="subtle" variant="titles">
222+
NAME
223+
</Text>
224+
<Text color="subtle" variant="titles">
225+
VALUE
226+
</Text>
227+
228+
{dnsRecords.map((record, index) => (
229+
<Fragment key={index}>
230+
<InputEllipsis readOnly value={record.type} />
231+
<InputEllipsis
232+
readOnly
233+
value={record.host}
234+
suffix={
235+
<CopyToClipboard text={record.host}>
236+
<NestedInputButton type="button">
237+
<CopyIcon />
238+
</NestedInputButton>
239+
</CopyToClipboard>
240+
}
241+
/>
242+
<InputEllipsis
243+
readOnly
244+
value={record.value}
245+
suffix={
246+
<CopyToClipboard text={record.value}>
247+
<NestedInputButton type="button">
248+
<CopyIcon />
249+
</NestedInputButton>
250+
</CopyToClipboard>
251+
}
252+
/>
253+
</Fragment>
254+
))}
255+
</Grid>
256+
257+
<Grid
258+
gap={2}
259+
align={"center"}
260+
css={{
261+
gridTemplateColumns: `1fr auto 1fr`,
262+
}}
263+
>
264+
<Separator css={{ alignSelf: "unset" }} />
265+
<Text color="main">OR</Text>
266+
<Separator css={{ alignSelf: "unset" }} />
267+
</Grid>
268+
269+
<Entri
270+
dnsRecords={dnsRecords}
271+
domain={projectDomain.domain}
272+
onClose={() => {
273+
// Sometimes Entri modal dialog hangs even if it's successful,
274+
// until they fix that, we'll just refresh the status here on every onClose event
275+
onUpdateStatus();
276+
}}
277+
/>
278+
</>
279+
);
280+
};
281+
167282
const DomainItem = (props: {
168283
initiallyOpen: boolean;
169284
projectDomain: Domain;
@@ -272,33 +387,8 @@ const DomainItem = (props: {
272387
});
273388
}, [status, handleVerify, handleUpdateStatus, isStatusLoading]);
274389

275-
const publisherHost = useStore($publisherHost);
276-
const cnameEntryName = extractCname(props.projectDomain.domain);
277-
const cnameEntryValue = `${props.projectDomain.cname}.customers.${publisherHost}`;
278-
279-
const txtEntryName =
280-
cnameEntryName === "@"
281-
? "_webstudio_is"
282-
: `_webstudio_is.${cnameEntryName}`;
283-
284390
const domainStatus = getStatus(props.projectDomain);
285391

286-
const cnameRecord = {
287-
type: "CNAME",
288-
host: cnameEntryName,
289-
value: cnameEntryValue,
290-
ttl: 300,
291-
} as const;
292-
293-
const txtRecord = {
294-
type: "TXT",
295-
host: txtEntryName,
296-
value: props.projectDomain.expectedTxtRecord,
297-
ttl: 300,
298-
} as const;
299-
300-
const dnsRecords = [cnameRecord, txtRecord];
301-
302392
const { isVerifiedActive, text } = getStatusText({
303393
projectDomain: props.projectDomain,
304394
isLoading: false,
@@ -413,98 +503,9 @@ const DomainItem = (props: {
413503
)}
414504
</Grid>
415505

416-
<Text color="subtle">
417-
<strong>To verify your domain:</strong>
418-
<br />
419-
Visit the admin console of your domain registrar (the website you
420-
purchased your domain from) and create one <strong>CNAME</strong>{" "}
421-
record and one <strong>TXT</strong> record with the values shown
422-
below:
423-
</Text>
424-
425-
<Grid
426-
gap={2}
427-
css={{
428-
gridTemplateColumns: `${theme.spacing[18]} 1fr 1fr`,
429-
}}
430-
>
431-
<Text color="subtle" variant={"titles"}>
432-
TYPE
433-
</Text>
434-
<Text color="subtle" variant={"titles"}>
435-
NAME
436-
</Text>
437-
<Text color="subtle" variant={"titles"}>
438-
VALUE
439-
</Text>
440-
441-
<InputEllipsis readOnly value="CNAME" />
442-
<InputEllipsis
443-
readOnly
444-
value={cnameRecord.host}
445-
suffix={
446-
<CopyToClipboard text={cnameRecord.host}>
447-
<NestedInputButton type="button">
448-
<CopyIcon />
449-
</NestedInputButton>
450-
</CopyToClipboard>
451-
}
452-
/>
453-
<InputEllipsis
454-
readOnly
455-
value={cnameRecord.value}
456-
suffix={
457-
<CopyToClipboard text={cnameRecord.value}>
458-
<NestedInputButton type="button">
459-
<CopyIcon />
460-
</NestedInputButton>
461-
</CopyToClipboard>
462-
}
463-
/>
464-
465-
<InputEllipsis readOnly value="TXT" />
466-
<InputEllipsis
467-
readOnly
468-
value={txtRecord.host}
469-
suffix={
470-
<CopyToClipboard text={txtRecord.host}>
471-
<NestedInputButton type="button">
472-
<CopyIcon />
473-
</NestedInputButton>
474-
</CopyToClipboard>
475-
}
476-
/>
477-
<InputEllipsis
478-
readOnly
479-
value={txtRecord.value}
480-
suffix={
481-
<CopyToClipboard text={txtRecord.value}>
482-
<NestedInputButton type="button">
483-
<CopyIcon />
484-
</NestedInputButton>
485-
</CopyToClipboard>
486-
}
487-
/>
488-
</Grid>
489-
490-
<Grid
491-
gap={2}
492-
align={"center"}
493-
css={{
494-
gridTemplateColumns: `1fr auto 1fr`,
495-
}}
496-
>
497-
<Separator css={{ alignSelf: "unset" }} />
498-
<Text color="main">OR</Text>
499-
<Separator css={{ alignSelf: "unset" }} />
500-
</Grid>
501-
502-
<Entri
503-
dnsRecords={dnsRecords}
504-
domain={props.projectDomain.domain}
505-
onClose={() => {
506-
// Sometimes Entri modal dialog hangs even if it's successful,
507-
// until they fix that, we'll just refresh the status here on every onClose event
506+
<DomainConfig
507+
projectDomain={props.projectDomain}
508+
onUpdateStatus={() => {
508509
if (status === "UNVERIFIED") {
509510
startTransition(async () => {
510511
await handleVerify();

apps/builder/app/builder/features/topbar/entri.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { trpcClient } from "~/shared/trpc/trpc-client";
55

66
// https://developers.entri.com/docs/install
77
type DnsRecord = {
8-
type: "CNAME" | "TXT";
8+
type: "CNAME" | "ALIAS" | "TXT";
99
host: string;
1010
value: string;
1111
ttl: number;
@@ -27,7 +27,7 @@ declare global {
2727
const entriGlobalStyles = globalCss({
2828
body: {
2929
"&>#entriApp": {
30-
pointerEvents: "all",
30+
pointerEvents: "auto",
3131
},
3232
},
3333
});
@@ -87,15 +87,13 @@ const useEntri = ({ domain, dnsRecords, onClose }: EntriProps) => {
8787
export const Entri = ({ domain, dnsRecords, onClose }: EntriProps) => {
8888
entriGlobalStyles();
8989
const { error, isOpen, showDialog } = useEntri({
90-
onClose,
9190
domain,
9291
dnsRecords,
92+
onClose,
9393
});
94-
9594
return (
9695
<>
9796
{error !== undefined && <Text color="destructive">{error}</Text>}
98-
9997
<Button
10098
disabled={isOpen}
10199
color="neutral"

apps/builder/app/builder/features/topbar/publish.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ import {
7979
parseComponentName,
8080
type Templates,
8181
} from "@webstudio-is/sdk";
82-
import DomainCheckbox, { domainToPublishName } from "./domain-checkbox";
82+
import { DomainCheckbox, domainToPublishName } from "./domain-checkbox";
8383
import { CopyToClipboard } from "~/builder/shared/copy-to-clipboard";
8484
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
8585
import { RelativeTime } from "~/builder/shared/relative-time";

apps/builder/app/services/trcp-router.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { router } from "@webstudio-is/trpc-interface/index.server";
2-
import { marketplaceRouter } from "../shared/marketplace/router.server";
32
import { domainRouter } from "@webstudio-is/domain/index.server";
43
import { projectRouter } from "@webstudio-is/project/index.server";
54
import { authorizationTokenRouter } from "@webstudio-is/authorization-token/index.server";
65
import { dashboardProjectRouter } from "@webstudio-is/dashboard/index.server";
6+
import { marketplaceRouter } from "../shared/marketplace/router.server";
77
import { logoutRouter } from "./logout-router.server";
88

99
export const appRouter = router({

0 commit comments

Comments
 (0)