mirror of
https://github.com/rustfs/console.git
synced 2026-08-29 03:52:28 +08:00
feat: add UTM params to external links (#168)
This commit is contained in:
@@ -18,6 +18,7 @@ import { buildRoute, getLoginRoute } from "@/lib/routes"
|
||||
import { configManager } from "@/lib/config"
|
||||
import { checkServerHealth } from "@/lib/config-helpers"
|
||||
import { getThemeManifest } from "@/lib/theme/manifest"
|
||||
import { withUtm } from "@/lib/utm"
|
||||
|
||||
export default function ConfigPage() {
|
||||
return (
|
||||
@@ -32,7 +33,7 @@ function ConfigPageContent() {
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const theme = getThemeManifest()
|
||||
const docs = theme.links.documentation ?? "https://docs.rustfs.com/?ref=console"
|
||||
const docs = withUtm(theme.links.documentation ?? "https://docs.rustfs.com/?ref=console", "config-page")
|
||||
|
||||
const [serverHost, setServerHost] = useState(() =>
|
||||
typeof window !== "undefined" ? (localStorage.getItem("rustfs-server-host") ?? "") : "",
|
||||
|
||||
@@ -32,6 +32,7 @@ import { canAccessDashboardRoute } from "@/lib/dashboard-route-meta"
|
||||
import { SidebarVersion } from "@/components/sidebars/version"
|
||||
import { useDirection } from "@/components/ui/direction"
|
||||
import { getThemeManifest } from "@/lib/theme/manifest"
|
||||
import { withUtm } from "@/lib/utm"
|
||||
import { ThemeLogo } from "@/components/theme/logo"
|
||||
|
||||
const theme = getThemeManifest()
|
||||
@@ -200,7 +201,7 @@ export function AppSidebar() {
|
||||
size="sm"
|
||||
render={
|
||||
<a
|
||||
href={normalizedTo(child)}
|
||||
href={withUtm(normalizedTo(child), "sidebar")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex w-full items-center gap-2"
|
||||
@@ -238,7 +239,7 @@ export function AppSidebar() {
|
||||
tooltip={getLabel(item)}
|
||||
render={
|
||||
<a
|
||||
href={normalizedTo(item)}
|
||||
href={withUtm(normalizedTo(item), "sidebar")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex w-full items-center gap-3"
|
||||
|
||||
@@ -5,6 +5,7 @@ import Link from "next/link"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ThemeLogo } from "@/components/theme/logo"
|
||||
import { getThemeManifest } from "@/lib/theme/manifest"
|
||||
import { withUtm } from "@/lib/utm"
|
||||
import { buildRoute } from "@/lib/routes"
|
||||
import { RiArrowRightLongFill } from "@remixicon/react"
|
||||
import { FlipWords } from "@/components/ui/flip-words"
|
||||
@@ -35,7 +36,7 @@ export function AuthHeroStatic() {
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
href="https://www.rustfs.com"
|
||||
href={withUtm(theme.links.website ?? "https://www.rustfs.com", "login-hero")}
|
||||
className="z-10 inline-flex h-10 w-max items-center gap-2 rounded-none border border-primary/30 px-5 leading-none text-primary transition-colors hover:bg-accent"
|
||||
>
|
||||
<span>{t("Visit website")}</span>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ThemeLogo } from "@/components/theme/logo"
|
||||
import { AuthHeroStatic } from "@/components/auth/heroes/hero-static"
|
||||
import { buildRoute } from "@/lib/routes"
|
||||
import { getThemeManifest } from "@/lib/theme/manifest"
|
||||
import { withUtm } from "@/lib/utm"
|
||||
|
||||
import type { OidcProvider } from "@/types/config"
|
||||
|
||||
@@ -54,7 +55,7 @@ export function LoginForm({
|
||||
}: LoginFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const theme = getThemeManifest()
|
||||
const website = theme.links.website ?? "https://www.rustfs.com"
|
||||
const website = withUtm(theme.links.website ?? "https://www.rustfs.com", "login-form")
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-dvh flex-col items-center justify-center overflow-hidden bg-muted p-4 sm:p-8 lg:p-12">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Card, CardContent } from "@/components/ui/card"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { DataTable } from "@/components/data-table/data-table"
|
||||
import { useDataTable } from "@/hooks/use-data-table"
|
||||
import { withUtm } from "@/lib/utm"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
|
||||
const hasValidLicense = false
|
||||
@@ -208,7 +209,10 @@ export function LicenseEnterpriseSection() {
|
||||
|
||||
const contactSupport = () => {
|
||||
window.open(
|
||||
"https://ww18.53kf.com/webCompany.php?arg=11003151&kf_sign=DA4MDMTc0Ng4MjE1MjEzODAyNDkyMDAyNzMwMDMxNTE%253D&style=2",
|
||||
withUtm(
|
||||
"https://ww18.53kf.com/webCompany.php?arg=11003151&kf_sign=DA4MDMTc0Ng4MjE1MjEzODAyNDkyMDAyNzMwMDMxNTE%253D&style=2",
|
||||
"license-page",
|
||||
),
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { RiGithubLine } from "@remixicon/react"
|
||||
import { getThemeManifest } from "@/lib/theme/manifest"
|
||||
import { withUtm } from "@/lib/utm"
|
||||
|
||||
export function LinksGithub() {
|
||||
const github = getThemeManifest().links.github
|
||||
@@ -13,7 +14,7 @@ export function LinksGithub() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
nativeButton={false}
|
||||
render={<a href={github} target="_blank" rel="noopener noreferrer" aria-label="GitHub" />}
|
||||
render={<a href={withUtm(github, "top-nav")} target="_blank" rel="noopener noreferrer" aria-label="GitHub" />}
|
||||
>
|
||||
<RiGithubLine className="size-4" aria-hidden />
|
||||
</Button>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { RiTwitterXLine } from "@remixicon/react"
|
||||
import { getThemeManifest } from "@/lib/theme/manifest"
|
||||
import { withUtm } from "@/lib/utm"
|
||||
|
||||
export function LinksX() {
|
||||
const x = getThemeManifest().links.x
|
||||
@@ -13,7 +14,7 @@ export function LinksX() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
nativeButton={false}
|
||||
render={<a href={x} target="_blank" rel="noopener noreferrer" aria-label="X" />}
|
||||
render={<a href={withUtm(x, "top-nav")} target="_blank" rel="noopener noreferrer" aria-label="X" />}
|
||||
>
|
||||
<RiTwitterXLine className="size-4" aria-hidden />
|
||||
</Button>
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
const UTM_SOURCE = "rustfs-console"
|
||||
const UTM_MEDIUM = "referral"
|
||||
|
||||
/**
|
||||
* Append UTM tracking params to an external (http/https) URL.
|
||||
*
|
||||
* URLs that already carry any utm_* param are returned unchanged so
|
||||
* theme authors keep full control over their own tracking.
|
||||
*
|
||||
* @param content optional placement label (e.g. "sidebar") for utm_content
|
||||
*/
|
||||
export function withUtm(url: string, content?: string): string {
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(url)
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return url
|
||||
}
|
||||
|
||||
for (const key of parsed.searchParams.keys()) {
|
||||
if (key.toLowerCase().startsWith("utm_")) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const utm = new URLSearchParams({ utm_source: UTM_SOURCE, utm_medium: UTM_MEDIUM })
|
||||
if (content) {
|
||||
utm.set("utm_content", content)
|
||||
}
|
||||
|
||||
// Concatenate instead of searchParams.set() so existing params stay
|
||||
// byte-identical (some third-party URLs carry signed values).
|
||||
parsed.search = parsed.search ? `${parsed.search}&${utm}` : `?${utm}`
|
||||
return parsed.toString()
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { withUtm } from "../../lib/utm"
|
||||
|
||||
test("withUtm appends utm params to a bare external URL", () => {
|
||||
assert.equal(
|
||||
withUtm("https://www.rustfs.com"),
|
||||
"https://www.rustfs.com/?utm_source=rustfs-console&utm_medium=referral",
|
||||
)
|
||||
})
|
||||
|
||||
test("withUtm sets utm_content when a placement is given", () => {
|
||||
assert.equal(
|
||||
withUtm("https://www.rustfs.com", "login-hero"),
|
||||
"https://www.rustfs.com/?utm_source=rustfs-console&utm_medium=referral&utm_content=login-hero",
|
||||
)
|
||||
})
|
||||
|
||||
test("withUtm preserves existing query params", () => {
|
||||
assert.equal(
|
||||
withUtm("https://docs.rustfs.com/?ref=console", "sidebar"),
|
||||
"https://docs.rustfs.com/?ref=console&utm_source=rustfs-console&utm_medium=referral&utm_content=sidebar",
|
||||
)
|
||||
})
|
||||
|
||||
test("withUtm inserts params before the hash fragment", () => {
|
||||
assert.equal(
|
||||
withUtm("https://docs.rustfs.com/guide#install"),
|
||||
"https://docs.rustfs.com/guide?utm_source=rustfs-console&utm_medium=referral#install",
|
||||
)
|
||||
})
|
||||
|
||||
test("withUtm keeps percent-encoded param values byte-identical", () => {
|
||||
assert.equal(
|
||||
withUtm("https://example.com/page.php?sign=AbC%253D&style=2"),
|
||||
"https://example.com/page.php?sign=AbC%253D&style=2&utm_source=rustfs-console&utm_medium=referral",
|
||||
)
|
||||
})
|
||||
|
||||
test("withUtm leaves URLs that already carry utm params unchanged", () => {
|
||||
assert.equal(
|
||||
withUtm("https://www.rustfs.com/?utm_source=partner", "sidebar"),
|
||||
"https://www.rustfs.com/?utm_source=partner",
|
||||
)
|
||||
})
|
||||
|
||||
test("withUtm leaves relative URLs unchanged", () => {
|
||||
assert.equal(withUtm("/browser", "sidebar"), "/browser")
|
||||
})
|
||||
|
||||
test("withUtm leaves non-http protocols unchanged", () => {
|
||||
assert.equal(withUtm("mailto:hello@rustfs.com"), "mailto:hello@rustfs.com")
|
||||
})
|
||||
Reference in New Issue
Block a user