fix: accept full URLs in rank tracking domain input (#117)
Replace DOMAIN_REGEX with URL-based normalization so users can paste full URLs (e.g. https://example.com/path) into the domain config form without getting a ZodError. The URL class handles protocol stripping, hostname extraction, and validation natively.
This commit is contained in:
parent
86d3714846
commit
e78ef5e3dd
@ -10,6 +10,7 @@ import { Modal } from "@/client/components/Modal";
|
||||
import { getStandardErrorMessage } from "@/client/lib/error-messages";
|
||||
import { captureClientEvent } from "@/client/lib/posthog";
|
||||
import type { RankTrackingConfig } from "@/types/schemas/rank-tracking";
|
||||
import { normalizeDomain } from "@/types/schemas/domain";
|
||||
import {
|
||||
depthToPages,
|
||||
pagesToDepth,
|
||||
@ -155,6 +156,7 @@ export function RankTrackingConfigModal({
|
||||
className="input input-bordered w-full"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
onBlur={() => setDomain(normalizeDomain(domain))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,7 +1,35 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const DOMAIN_REGEX =
|
||||
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
|
||||
/**
|
||||
* Extract and validate a bare hostname from user input that may be a full URL.
|
||||
* Strips protocol, www prefix, path, query-string, and hash.
|
||||
*/
|
||||
export function normalizeDomain(input: string): string {
|
||||
let d = input.trim().toLowerCase();
|
||||
// Ensure URL() can parse the input by adding a protocol if missing
|
||||
if (!/^[a-z]+:\/\//.test(d)) d = `https://${d}`;
|
||||
const { hostname } = new URL(d); // throws on truly invalid input
|
||||
return hostname.replace(/^www\./, "");
|
||||
}
|
||||
|
||||
/** Zod field: accepts a bare domain or full URL, outputs a clean hostname. */
|
||||
export const domainField = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(253)
|
||||
.transform((val, ctx) => {
|
||||
try {
|
||||
const hostname = normalizeDomain(val);
|
||||
if (!hostname.includes(".")) {
|
||||
ctx.addIssue({ code: "custom", message: "Invalid domain format" });
|
||||
return z.NEVER;
|
||||
}
|
||||
return hostname;
|
||||
} catch {
|
||||
ctx.addIssue({ code: "custom", message: "Invalid domain format" });
|
||||
return z.NEVER;
|
||||
}
|
||||
});
|
||||
|
||||
const booleanSearchParamSchema = z
|
||||
.union([z.boolean(), z.enum(["true", "false"])])
|
||||
@ -25,11 +53,7 @@ const domainTabs = ["keywords", "pages"] as const;
|
||||
|
||||
export const domainKeywordSuggestionsSchema = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
domain: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(253)
|
||||
.regex(DOMAIN_REGEX, "Invalid domain format"),
|
||||
domain: domainField,
|
||||
locationCode: z.number().int().positive(),
|
||||
languageCode: z.string().min(2).max(8),
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { rankTrackingConfigs } from "@/db/app.schema";
|
||||
import { DOMAIN_REGEX } from "@/types/schemas/domain";
|
||||
import { domainField } from "@/types/schemas/domain";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DB-derived types
|
||||
@ -50,11 +50,7 @@ export const getConfigsSchema = z.object({
|
||||
|
||||
export const createConfigSchema = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
domain: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(253)
|
||||
.regex(DOMAIN_REGEX, "Invalid domain format"),
|
||||
domain: domainField,
|
||||
locationCode: z.number().int().positive().optional(),
|
||||
languageCode: z.string().max(10).optional(),
|
||||
devices: devicesEnum.optional(),
|
||||
@ -65,12 +61,7 @@ export const createConfigSchema = z.object({
|
||||
export const updateConfigSchema = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
configId: z.string().uuid(),
|
||||
domain: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(253)
|
||||
.regex(DOMAIN_REGEX, "Invalid domain format")
|
||||
.optional(),
|
||||
domain: domainField.optional(),
|
||||
locationCode: z.number().int().positive().optional(),
|
||||
languageCode: z.string().max(10).optional(),
|
||||
devices: devicesEnum.optional(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user