Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/parking/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { toTenantParams } from "@/lib/parking";
import TenantPage, { generateMetadata as tenantMetadata } from "../page";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

type SearchParams = Record<string, string | undefined>;

/**
* /parking?name=<domain> — the URL registrar parking pages and forwarding rules
* point at. It renders the tenant page in place rather than redirecting to
* /?dn=<domain>, so the link a registrar already holds keeps working and masked
* forwarding never sees a hop. All other params (?ref, ?brand, ?social_*, …)
* pass straight through to the same renderer as /.
*/
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}): Promise<Metadata> {
const sp = (await searchParams) || {};
return tenantMetadata({ searchParams: Promise.resolve(toTenantParams(sp)) });
}

export default async function Parking({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const sp = (await searchParams) || {};
// No usable name → the tenant renderer falls back to <Landing />, not a 404.
return TenantPage({ searchParams: Promise.resolve(toTenantParams(sp)) });
}
24 changes: 24 additions & 0 deletions lib/parking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export type ParkingParams = Record<string, string | undefined>;

/**
* Registrar parking/forwarding links point at /parking?name=<domain>, while the
* tenant renderer at / keys off ?dn=. Map name → dn so both URLs drive one
* implementation instead of two that drift.
*
* Porkbun's param forwarding can glue the visitor's query onto the value
* ("scrambled.eggs?ref=abc"), and safeDomain() strips that back to a bare
* domain — so lift any glued ?ref= into its own param before it's lost. An
* explicit ?ref= already on the URL wins, matching the first-touch rule.
*/
export function toTenantParams(sp: ParkingParams): ParkingParams {
const { name, ...rest } = sp;
const raw = typeof name === "string" && name.trim() ? name : rest.dn;
if (typeof raw !== "string" || !raw.trim()) return rest;

const out: ParkingParams = { ...rest, dn: raw };
if (!out.ref) {
const glued = raw.match(/[?&]ref=([A-Za-z0-9_-]+)/)?.[1];
if (glued) out.ref = glued;
}
return out;
}
9 changes: 6 additions & 3 deletions middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ function frameAncestors(req: NextRequest): string {
// its own pages. New parked domains work without hand-editing FRAME_ANCESTORS.
const isDomain = (d: string) =>
d.length <= 253 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/.test(d);
for (const key of ["dn", "bid"]) {
// "name" is the same domain arriving via /parking?name=<self>.
for (const key of ["dn", "bid", "name"]) {
const d = (req.nextUrl.searchParams.get(key) || "").trim().toLowerCase();
if (isDomain(d)) allow.push(`https://${d}`, `https://www.${d}`);
}
Expand All @@ -39,9 +40,11 @@ export function middleware(req: NextRequest) {
// — the first ref a visitor arrives with sticks). Signup/waitlist read it so
// the referral is credited whenever they convert within the window.
// Normally ?ref=<code> is its own param. But Porkbun param-forwarding can glue
// it onto the ?dn= value ("dn=moshscript.com?ref=abc"), so recover it there too.
// it onto the ?dn= value ("dn=moshscript.com?ref=abc"), so recover it there too
// — and from ?name=, which carries the same domain on /parking.
const sp = req.nextUrl.searchParams;
const rawRef = sp.get("ref") ?? sp.get("dn")?.match(/[?&]ref=([A-Za-z0-9_-]+)/)?.[1] ?? null;
const glued = (key: string) => sp.get(key)?.match(/[?&]ref=([A-Za-z0-9_-]+)/)?.[1];
const rawRef = sp.get("ref") ?? glued("dn") ?? glued("name") ?? null;
if (rawRef && !req.cookies.get("mc_ref")?.value) {
const ref = rawRef.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 64);
if (ref) {
Expand Down
48 changes: 48 additions & 0 deletions tests/parking-params.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import test from "node:test";

import { toTenantParams } from "../lib/parking.ts";
import { safeDomain } from "../lib/config.ts";

test("toTenantParams maps ?name= onto the ?dn= the tenant renderer reads", () => {
assert.deepEqual(toTenantParams({ name: "scrambled.eggs" }), { dn: "scrambled.eggs" });
assert.equal(safeDomain(toTenantParams({ name: "scrambled.eggs" }).dn), "scrambled.eggs");
});

test("toTenantParams passes every other param through untouched", () => {
const out = toTenantParams({
name: "nonhuman.aliens",
brand: "Nonhuman",
ref: "abc123",
social_bluesky: "@x",
link_1: "https://example.com",
});
assert.equal(out.dn, "nonhuman.aliens");
assert.equal(out.brand, "Nonhuman");
assert.equal(out.ref, "abc123");
assert.equal(out.social_bluesky, "@x");
assert.equal(out.link_1, "https://example.com");
assert.equal(out.name, undefined);
});

test("toTenantParams recovers a Porkbun-glued ref off the name value", () => {
const out = toTenantParams({ name: "scrambled.eggs?ref=abc123" });
assert.equal(out.ref, "abc123");
// safeDomain still resolves the tenant from the glued value.
assert.equal(safeDomain(out.dn), "scrambled.eggs");
});

test("an explicit ref wins over a glued one (first-touch stays predictable)", () => {
const out = toTenantParams({ name: "scrambled.eggs?ref=glued", ref: "explicit" });
assert.equal(out.ref, "explicit");
});

test("toTenantParams leaves an existing ?dn= alone when no name is given", () => {
assert.deepEqual(toTenantParams({ dn: "moshcode.sh" }), { dn: "moshcode.sh" });
});

test("a blank or missing name yields no dn, so / falls back to the landing page", () => {
assert.equal(toTenantParams({}).dn, undefined);
assert.equal(toTenantParams({ name: " " }).dn, undefined);
assert.equal(toTenantParams({ name: "" }).dn, undefined);
});
Loading