diff --git a/app/components/CopyTracking.tsx b/app/components/CopyTracking.tsx new file mode 100644 index 0000000..42a5f1d --- /dev/null +++ b/app/components/CopyTracking.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useEffect } from "react"; + +export function CopyTracking() { + useEffect(() => { + // 监听全局 Copy 事件 + const handleCopy = () => { + const selection = window.getSelection(); + if (!selection || selection.isCollapsed) return; + + const text = selection.toString(); + if (!text) return; + + // Determine if it's code or text + let type = "text"; + const anchorNode = selection.anchorNode; + const focusNode = selection.focusNode; + + const isCode = (node: Node | null) => { + let current = node; + while (current) { + if ( + current.nodeName === "PRE" || + current.nodeName === "CODE" || + (current instanceof Element && + (current.classList.contains("code-block") || + current.tagName === "PRE" || + current.tagName === "CODE")) + ) { + return true; + } + current = current.parentNode; + } + return false; + }; + + if (isCode(anchorNode) || isCode(focusNode)) { + // 判断选中节点是否包含在
或 标签内,区分代码块复制
+ type = "code";
+ }
+
+ // Umami 埋点: 记录复制行为,区分文本/代码类型和复制长度
+ if (window.umami) {
+ window.umami.track("content_copy", {
+ type,
+ content_length: text.length,
+ });
+ }
+ };
+
+ document.addEventListener("copy", handleCopy);
+ return () => document.removeEventListener("copy", handleCopy);
+ }, []);
+
+ return null;
+}
diff --git a/app/components/CustomSearchDialog.tsx b/app/components/CustomSearchDialog.tsx
index c7a4bd3..c155a58 100644
--- a/app/components/CustomSearchDialog.tsx
+++ b/app/components/CustomSearchDialog.tsx
@@ -17,6 +17,7 @@ import {
TagsListItem,
type SharedProps,
} from "fumadocs-ui/components/dialog/search";
+import { useRouter } from "next/navigation";
interface TagItem {
name: string;
@@ -34,6 +35,12 @@ interface DefaultSearchDialogProps extends SharedProps {
allowClear?: boolean;
}
+interface SearchItem {
+ url?: string;
+ onSelect?: (value: string) => void;
+ [key: string]: unknown;
+}
+
export function CustomSearchDialog({
defaultTag,
tags = [],
@@ -46,7 +53,12 @@ export function CustomSearchDialog({
...props
}: DefaultSearchDialogProps) {
const { locale } = useI18n();
+ const router = useRouter();
const [tag, setTag] = useState(defaultTag);
+
+ // Extract onOpenChange to use in dependency array cleanly
+ const { onOpenChange, ...otherProps } = props;
+
const { search, setSearch, query } = useDocsSearch(
type === "fetch"
? {
@@ -65,11 +77,12 @@ export function CustomSearchDialog({
},
);
- // Tracking logic
+ // Tracking logic for queries
useEffect(() => {
if (!search) return;
const timer = setTimeout(() => {
+ // Umami 埋点: 搜索结果点击
if (window.umami) {
window.umami.track("search_query", { query: search });
}
@@ -88,12 +101,48 @@ export function CustomSearchDialog({
}));
}, [links]);
+ // 使用 useMemo 劫持 search items,注入埋点逻辑
+ const trackedItems = useMemo(() => {
+ const data = query.data !== "empty" && query.data ? query.data : defaultItems;
+ if (!data) return [];
+
+ return data.map((item: unknown, index: number) => {
+ const searchItem = item as SearchItem;
+ return {
+ ...searchItem,
+ onSelect: (value: string) => {
+ // Umami 埋点: 搜索结果点击
+ if (window.umami) {
+ window.umami.track("search_result_click", {
+ query: search,
+ rank: index + 1,
+ url: searchItem.url,
+ });
+ }
+
+ // Call original onSelect if it exists
+ if (searchItem.onSelect) searchItem.onSelect(value);
+
+ // Handle navigation if URL exists
+ if (searchItem.url) {
+ // 显式执行路由跳转和关闭弹窗,确保点击行为能够同时触发埋点和导航
+ router.push(searchItem.url);
+ if (onOpenChange) {
+ onOpenChange(false);
+ }
+ }
+ },
+ };
+ });
+ }, [query.data, defaultItems, search, router, onOpenChange]);
+
return (
@@ -102,11 +151,8 @@ export function CustomSearchDialog({
-
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+
{tags.length > 0 && (
diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx
index 61336da..14e6ae1 100644
--- a/app/components/Footer.tsx
+++ b/app/components/Footer.tsx
@@ -63,6 +63,7 @@ export function Footer() {
Mission Brief
@@ -136,6 +144,10 @@ export function Footer() {
Network Status
diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx
index b2831a8..45f65c9 100644
--- a/app/components/Hero.tsx
+++ b/app/components/Hero.tsx
@@ -83,9 +83,10 @@ export function Hero() {