Skip to content

Commit 3a005d0

Browse files
ralyodioclaude
andauthored
Serve branded parked domains directly (Host-based tenant resolution) (#17)
* Use branded <dn>/… links for bid + affiliate on tenant pages Inside a masked parked-domain iframe, the bid link was a relative /?bid=<dn>, so it navigated the iframe to moshcoding.com/?bid=<dn> — which refuses to be framed (no ?dn → CSP frame-ancestors blocks it): 'moshcoding.com refused to connect'. And the affiliate share link exposed moshcoding.com/?dn=<dn>&ref=<code>. Both now use the branded domain URL: - Bid link → https://<dn>/?bid=<dn> with target=_top (breaks out of the iframe to the branded domain instead of loading moshcoding.com in-frame) - Affiliate share link → https://<dn>/?ref=<code> - middleware also auto-allows the ?bid=<domain> as a frame-ancestor (like ?dn), so a forwarded bid page renders in-frame Note: the domain's forwarding must pass query params through (Porkbun masked forwarding with path/query, or param forwarding) for ?ref/?bid to reach the app; safeDomain already handles the appended-query case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Resolve tenant by Host header (serve branded domains directly, no iframe) Porkbun MASKED forwarding serves a static frameset with a FIXED src (moshcoding.com/?dn=<self>) and drops the visitor's query — so ?ref/?bid never reach the app. That's unfixable with masked forwarding. Fix: when the app is served DIRECTLY on a branded custom domain (Host is not moshcoding.com / *.railway.app / localhost), resolve the tenant from the Host header. The domain then renders its own tenant page natively — no iframe, no CSP, and ?ref/?bid arrive intact. ?dn still wins (keeps the masked-iframe path working), so this is backward-compatible. Verified: Host moshscript.com → moshscript tenant page; +?ref sets the 90-day mc_ref cookie natively; +?bid → BidPage; Host moshcoding.com → landing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0e11c2d commit 3a005d0

1 file changed

Lines changed: 22 additions & 2 deletions

File tree

app/page.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Metadata } from "next";
2+
import { headers } from "next/headers";
23
import { configFor, safeDomain } from "@/lib/config";
34
import { getTenantConfig } from "@/lib/db";
45
import Landing from "@/components/Landing";
@@ -8,6 +9,24 @@ import BidPage from "@/components/BidPage";
89
export const runtime = "nodejs";
910
export const dynamic = "force-dynamic";
1011

12+
/**
13+
* The parked domain from the Host header, when the app is served DIRECTLY on a
14+
* branded custom domain (not moshcoding.com / *.railway.app / localhost). Lets a
15+
* domain pointed straight at this service render its own tenant page natively —
16+
* no iframe, no CSP, and query params (?ref, ?bid) arrive intact. Returns null
17+
* for the main app host so moshcoding.com keeps its landing page.
18+
*/
19+
async function hostTenantDn(): Promise<string | null> {
20+
const h = await headers();
21+
const host = (h.get("host") || "").toLowerCase().split(":")[0].replace(/^www\./, "");
22+
if (!host) return null;
23+
const appHost = (process.env.APP_BASE_URL || "https://moshcoding.com")
24+
.replace(/^https?:\/\//, "").replace(/\/.*$/, "").toLowerCase().replace(/^www\./, "");
25+
if (host === appHost || host === "localhost" || host === "127.0.0.1") return null;
26+
if (/\.railway\.app$|\.up\.railway\.app$/.test(host)) return null;
27+
return safeDomain(host);
28+
}
29+
1130
// Per-tenant OpenGraph/Twitter with a branded, generated og:image so every
1231
// parked domain shares nicely. No ?dn= → the landing keeps the layout defaults.
1332
export async function generateMetadata({
@@ -16,7 +35,7 @@ export async function generateMetadata({
1635
searchParams: Promise<Record<string, string | undefined>>;
1736
}): Promise<Metadata> {
1837
const sp = (await searchParams) || {};
19-
const dn = safeDomain(sp.dn);
38+
const dn = safeDomain(sp.dn) || (await hostTenantDn());
2039
if (!dn) return {};
2140
const tenantOverride = await getTenantConfig(dn).catch(() => null);
2241
const cfg = configFor(dn, {
@@ -42,7 +61,8 @@ export default async function Page({
4261
const sp = (await searchParams) || {};
4362
const bidDn = safeDomain(sp.bid);
4463
if (bidDn) return <BidPage dn={bidDn} />;
45-
const dn = safeDomain(sp.dn);
64+
// ?dn wins (iframe/masked path); else the branded Host (direct custom domain).
65+
const dn = safeDomain(sp.dn) || (await hostTenantDn());
4666
if (!dn) return <Landing />;
4767

4868
// A paid/provisioned domain has a tenants row that overrides the defaults.

0 commit comments

Comments
 (0)