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
8 changes: 3 additions & 5 deletions apps/web/src/components/import-from-deco-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { useAutoInstallGitHub } from "@/hooks/use-auto-install-github";
import { useNavigateToAgent } from "@/hooks/use-navigate-to-agent";
import { resolveDecoSiteGithubRepo } from "@decocms/shared/deco-sites-github";
import {
defaultPreviewServerUrl,
pickProductionDomain,
productionUrlFromDomain,
} from "@decocms/shared/deco-site-production-url";
import { getOrgGithubConnections } from "@decocms/shared/github-repo-scope";
import {
Expand Down Expand Up @@ -271,10 +271,8 @@ export function ImportFromDecoDialog({
const projectIcon = connBody.icon ?? null;
const slug = generateSlug(siteName);
const siteSlug = siteName.toLowerCase();
// Default the preview server to the site's `{slug}.deco.site` host (legacy `productionUrl` dual-written for rollback).
const previewServerUrl = productionUrlFromDomain(
`${siteSlug}.deco.site`,
);
// Default to the `{slug}.deco.site` host (legacy `productionUrl` dual-written for rollback).
const previewServerUrl = defaultPreviewServerUrl(siteName);

// 2. Create a space (virtual MCP) wired to both admin-mcp and GitHub.
const result = (await client.callTool({
Expand Down
20 changes: 20 additions & 0 deletions packages/shared/src/deco-site-production-url.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test";
import {
defaultPreviewServerUrl,
pickProductionDomain,
productionUrlFromDomain,
resolvePreviewServerUrl,
Expand Down Expand Up @@ -82,6 +83,25 @@ describe("productionUrlFromDomain", () => {
});
});

describe("defaultPreviewServerUrl", () => {
it("slugifies a bare site name into the deco.site host", () => {
expect(defaultPreviewServerUrl("acme")).toBe("https://acme.deco.site/");
});

it("slugifies a display name with spaces and symbols", () => {
expect(defaultPreviewServerUrl("My Cool Site!")).toBe(
"https://my-cool-site.deco.site/",
);
});

it("returns null for empty / whitespace-only / nullish, instead of throwing", () => {
expect(defaultPreviewServerUrl("")).toBeNull();
expect(defaultPreviewServerUrl(" ")).toBeNull();
expect(defaultPreviewServerUrl(null)).toBeNull();
expect(defaultPreviewServerUrl(undefined)).toBeNull();
});
});

describe("resolvePreviewServerUrl", () => {
it("prefers previewServerUrl over the legacy productionUrl key", () => {
expect(
Expand Down
23 changes: 20 additions & 3 deletions packages/shared/src/deco-site-production-url.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { slugify } from "./utils/slugify";

/**
* Site-URL helpers for linked deco.cx sites.
*
Expand All @@ -6,9 +8,9 @@
* by imports before the rename and still read as a fallback). It is usually
* the live production site, but any deco-runtime deployment works (staging, a
* local `https://localhost:3100`, ...), which is why the canonical name is
* "preview server". We deliberately do NOT derive it from `siteSlug` (the
* `{slug}.deco.site` guess) — a site's real URL can be a custom domain, so we
* persist what deco.cx actually reports at import time.
* "preview server". Imports default it to the site's `{slug}.deco.site` guess
* (see `defaultPreviewServerUrl`) rather than a reported custom domain, so the
* preview always targets the deco-runtime host.
*/

/** Validate/normalize a stored site URL. Returns the canonical href or `null`. */
Expand Down Expand Up @@ -67,3 +69,18 @@ export function productionUrlFromDomain(
const withProtocol = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
return sanitizeSiteUrl(withProtocol);
}

/**
* Guess a site's deco.site preview host from its display name. `siteName` is
* a free-text title (shown as-is in the import picker), not a hostname, so it
* is slugified first — an unslugified name with a space or symbol makes
* `new URL()` throw, and `productionUrlFromDomain` swallows that into `null`,
* silently leaving the imported site with no preview server at all.
*/
export function defaultPreviewServerUrl(
siteName: string | null | undefined,
): string | null {
const slug = slugify(siteName ?? "");
if (!slug) return null;
return productionUrlFromDomain(`${slug}.deco.site`);
}
Loading