From 2a3c18165779c879029018481dbb8500fc29a39a Mon Sep 17 00:00:00 2001
From: Neal Shah <30693865+ProgrammerIn-wonderland@users.noreply.github.com>
Date: Fri, 30 Oct 2026 21:16:25 -0400
Subject: [PATCH] DNS ui changes (#4417)
* DNS ui changes
---
src/docs/src/Objects/customdomain.md | 14 ++++--
src/gui/src/UI/UIWindowAttachCustomDomain.js | 46 ++++++++++++++++++--
src/gui/src/css/dashboard.css | 10 +++++
src/gui/src/i18n/translations/en.js | 5 ++-
src/puter-js/src/modules/hosting/types.js | 9 +++-
5 files changed, 74 insertions(+), 10 deletions(-)
diff --git a/src/docs/src/Objects/customdomain.md b/src/docs/src/Objects/customdomain.md
index a7950774c..2675fea08 100644
--- a/src/docs/src/Objects/customdomain.md
+++ b/src/docs/src/Objects/customdomain.md
@@ -39,11 +39,11 @@ Why the domain isn't active yet, when that's known.
The DNS records to set at the domain's DNS provider. Each record is an object with these attributes:
-- `type` (String): `CNAME` or `TXT`.
+- `type` (String): `CNAME`, `TXT` or `A`.
- `name` (String): the record's name.
- `value` (String): the record's value.
-- `purpose` (String): `routing` for the record that sends traffic to the website, `ownership` for the record that proves the user owns the domain, or `certificate` for a record that speeds up issuing the HTTPS certificate.
-- `required` (Boolean): `true` for the `routing` and `ownership` records, which the user has to set. A `certificate` record is optional.
+- `purpose` (String): `routing` for the record that sends traffic to the website, `ownership` for the record that proves the user owns the domain, `certificate` for a record that speeds up issuing the HTTPS certificate, or `redirect` for the `A` record that forwards a root domain to its `www.` hostname.
+- `required` (Boolean): `true` for the `routing`, `ownership` and `redirect` records, which the user has to set. A `certificate` record is optional.
#### `createdAt` (Number)
@@ -52,3 +52,11 @@ When the domain was added, in milliseconds since the Unix epoch.
#### `activatedAt` (Number | null)
When the domain became active, in milliseconds since the Unix epoch, or `null` if it isn't active.
+
+#### `dnsProvider` (String, optional)
+
+Who hosts the DNS of a root domain: `route53`, `godaddy` or `other`. Puter looks this up when a root domain is added, and only includes it in that response. Route 53 and GoDaddy can't point a root domain at another host name, so a root domain on either is added as its `www.` hostname (see `redirectFrom`).
+
+#### `redirectFrom` (String, optional)
+
+The root domain that forwards to this `www.` hostname. A root domain hosted on Route 53 or GoDaddy is added as its `www.` hostname, and the user can switch any other root domain to `www.` in the dialog. The `records` then include an `A` record that forwards the root domain.
diff --git a/src/gui/src/UI/UIWindowAttachCustomDomain.js b/src/gui/src/UI/UIWindowAttachCustomDomain.js
index aa09bcf05..5bd13545c 100644
--- a/src/gui/src/UI/UIWindowAttachCustomDomain.js
+++ b/src/gui/src/UI/UIWindowAttachCustomDomain.js
@@ -136,7 +136,9 @@ async function UIWindowAttachCustomDomain (options) {
`);
+ const type = domain.isApex && record.purpose === 'routing'
+ ? text('custom_domain_record_type_apex')
+ : record.type;
$tr.find('.custom-domain-record-type').text(record.required
- ? record.type
- : `${record.type} ${i18n('custom_domain_optional', [], false)}`);
+ ? type
+ : `${type} ${text('custom_domain_optional')}`);
const [$name, $value] = $tr.find('code').toArray().map((el) => $(el));
$name.text(record.name);
$value.text(record.value);
@@ -405,8 +415,17 @@ async function UIWindowAttachCustomDomain (options) {
return;
}
// A domain that is no longer pending comes back with only its
- // routing record; keep the full set shown when it was added.
- chosen = { ...res.data, records: res.data.records?.length > 1 ? res.data.records : chosen.records };
+ // routing record, and a GET never repeats the provider or the
+ // redirect; keep what was shown when it was added.
+ const redirect = chosen.records.filter((r) => r.purpose === 'redirect');
+ const fresh = (res.data.records?.length > 1 ? res.data.records : chosen.records)
+ .filter((r) => r.purpose !== 'redirect');
+ chosen = {
+ ...res.data,
+ dnsProvider: chosen.dnsProvider,
+ redirectFrom: chosen.redirectFrom,
+ records: [...fresh.slice(0, 1), ...redirect, ...fresh.slice(1)],
+ };
renderRecords(chosen);
} catch ( e ) {
console.error('custom-domain: status check failed', e);
@@ -415,6 +434,25 @@ async function UIWindowAttachCustomDomain (options) {
setBusy(false);
}
});
+ // Same row, so the month already paid covers the www hostname.
+ $overlay.find('.custom-domain-use-www').on('click', async () => {
+ if ( busy || ! chosen ) return;
+ setBusy(true);
+ try {
+ const res = await api('POST', `/${encodeURIComponent(chosen.uuid)}/use-www`);
+ if ( ! res.ok ) {
+ showError('records', errorFor(res));
+ return;
+ }
+ chosen = res.data;
+ renderRecords(chosen);
+ } catch ( e ) {
+ console.error('custom-domain: switch to www failed', e);
+ showError('records', text('custom_domain_error_generic'));
+ } finally {
+ setBusy(false);
+ }
+ });
$overlay.find('.custom-domain-done').on('click', () => {
if ( ! busy ) close();
});
diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css
index 22ac6c8c6..c43eb30ab 100644
--- a/src/gui/src/css/dashboard.css
+++ b/src/gui/src/css/dashboard.css
@@ -2124,6 +2124,16 @@ input.myapps-group-name:focus {
border-top: 1px solid var(--afb-border);
vertical-align: top;
}
+.custom-domain-redirect strong {
+ color: var(--afb-fg);
+ word-break: break-all;
+}
+.custom-domain-use-www {
+ display: block;
+ width: 100%;
+ margin-top: 10px;
+ font-size: 13px;
+}
.custom-domain-copy {
display: block;
word-break: break-all;
diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js
index 6ad4ec1fe..95f1bdd58 100644
--- a/src/gui/src/i18n/translations/en.js
+++ b/src/gui/src/i18n/translations/en.js
@@ -160,7 +160,7 @@ const en = {
current_password: 'Current Password',
custom_domain_add: 'Add Domain',
custom_domain_add_new: 'Add a New Domain',
- custom_domain_apex_hint: 'This is a root domain. Your DNS provider needs to support CNAME flattening, ALIAS, or ANAME records for it. If it doesn\'t, connect www instead and forward the root domain to it.',
+ custom_domain_apex_hint: 'This is a root domain, so your DNS provider needs to support a CNAME, ALIAS or ANAME record at the root (sometimes called CNAME flattening).',
custom_domain_claim_c2a: 'Someone else added this domain but never connected it. If it\'s yours, add this record at your DNS provider, then verify. The domain moves to your account.',
custom_domain_claim_not_found: 'The record isn\'t visible yet. DNS changes can take a few minutes; try again shortly.',
custom_domain_check_status: 'Check Status',
@@ -181,7 +181,10 @@ const en = {
custom_domain_none_yet: 'You don\'t have any custom domains yet.',
custom_domain_optional: '(optional)',
custom_domain_price_note: 'Each domain costs %% per month. example.com and www.example.com count as two domains.',
+ custom_domain_no_root_alias: 'My DNS provider doesn\'t support CNAME, ALIAS or ANAME at the root',
custom_domain_record_type: 'Type',
+ custom_domain_record_type_apex: 'CNAME / ALIAS / ANAME',
+ custom_domain_redirect_note: 'Your website is served on %strong%. The A record forwards visitors from %strong% to it.',
custom_domain_record_value: 'Value',
custom_domain_records_c2a: 'Add these records at your DNS provider. The domain starts working a few minutes after they\'re in place.',
custom_domain_requested_by: 'Requested by %strong%',
diff --git a/src/puter-js/src/modules/hosting/types.js b/src/puter-js/src/modules/hosting/types.js
index d6f36729d..7fdacb101 100644
--- a/src/puter-js/src/modules/hosting/types.js
+++ b/src/puter-js/src/modules/hosting/types.js
@@ -15,10 +15,11 @@
* A DNS record the user sets at their DNS provider for a custom domain.
*
* @typedef {Object} CustomDomainRecord
- * @property {'CNAME' | 'TXT'} type Record type.
+ * @property {'CNAME' | 'TXT' | 'A'} type Record type.
* @property {string} name Record name (host).
* @property {string} value Record value.
- * @property {'routing' | 'ownership' | 'certificate'} purpose What the record is for.
+ * @property {'routing' | 'ownership' | 'certificate' | 'redirect'} purpose What the record is
+ * for. `redirect` is the A record that forwards a root domain to its `www.` hostname.
* @property {boolean} required `true` for records the user has to set (routing and
* ownership); certificate records only speed issuance up.
*/
@@ -38,6 +39,10 @@
* @property {CustomDomainRecord[]} records DNS records to set.
* @property {number} createdAt When the domain was added, in milliseconds since the epoch.
* @property {number | null} activatedAt When the domain went active, if it has.
+ * @property {'route53' | 'godaddy' | 'other'} [dnsProvider] Who hosts the DNS of a root
+ * domain, when Puter looked it up while adding it.
+ * @property {string} [redirectFrom] The root domain forwarded to this `www.` hostname, when
+ * the root domain couldn't be served directly.
*/
export {};