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
57 changes: 57 additions & 0 deletions app/components/CopyTracking.tsx
Original file line number Diff line number Diff line change
@@ -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)) {
// 判断选中节点是否包含在 <pre> 或 <code> 标签内,区分代码块复制
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;
}
60 changes: 53 additions & 7 deletions app/components/CustomSearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
TagsListItem,
type SharedProps,
} from "fumadocs-ui/components/dialog/search";
import { useRouter } from "next/navigation";

interface TagItem {
name: string;
Expand All @@ -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 = [],
Expand All @@ -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"
? {
Expand All @@ -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 });
}
Expand All @@ -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 (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
onOpenChange={onOpenChange}
{...otherProps}
>
<SearchDialogOverlay />
<SearchDialogContent>
Expand All @@ -102,11 +151,8 @@ export function CustomSearchDialog({
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList
items={
query.data !== "empty" && query.data ? query.data : defaultItems
}
/>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<SearchDialogList items={trackedItems as any} />
</SearchDialogContent>
<SearchDialogFooter>
{tags.length > 0 && (
Expand Down
12 changes: 12 additions & 0 deletions app/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function Footer() {
<Link
href="/docs/ai"
className="hover:text-[#CC0000] transition-colors"
// Umami 埋点: Footer 快速链接点击
data-umami-event="navigation_click"
data-umami-event-region="footer"
data-umami-event-label="AI & Mathematics"
Expand All @@ -74,6 +75,7 @@ export function Footer() {
<Link
href="/docs/computer-science"
className="hover:text-[#CC0000] transition-colors"
// Umami 埋点: Footer 快速链接点击
data-umami-event="navigation_click"
data-umami-event-region="footer"
data-umami-event-label="Computer Science"
Expand All @@ -85,6 +87,7 @@ export function Footer() {
<Link
href="/docs/CommunityShare"
className="hover:text-[#CC0000] transition-colors"
// Umami 埋点: Footer 快速链接点击
data-umami-event="navigation_click"
data-umami-event-region="footer"
data-umami-event-label="Community Sharing"
Expand All @@ -96,6 +99,7 @@ export function Footer() {
<Link
href="/docs/jobs"
className="hover:text-[#CC0000] transition-colors"
// Umami 埋点: Footer 快速链接点击
data-umami-event="navigation_click"
data-umami-event-region="footer"
data-umami-event-label="Career Prep"
Expand Down Expand Up @@ -128,6 +132,10 @@ export function Footer() {
<li>
<a
href="#features"
// Umami 埋点: Footer 快速链接点击
data-umami-event="navigation_click"
data-umami-event-region="footer"
data-umami-event-label="Mission Brief"
className="hover:text-[#CC0000] transition-colors"
>
Mission Brief
Expand All @@ -136,6 +144,10 @@ export function Footer() {
<li>
<a
href="#community"
// Umami 埋点: Footer 快速链接点击
data-umami-event="navigation_click"
data-umami-event-region="footer"
data-umami-event-label="Network Status"
className="hover:text-[#CC0000] transition-colors"
>
Network Status
Expand Down
12 changes: 7 additions & 5 deletions app/components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ export function Hero() {
<Link
href="/docs/ai"
className="block w-full"
data-umami-event="feature_cta_click"
data-umami-event-action="access_articles"
data-umami-event-location="hero_sidebar"
// Umami 埋点: Hero CTA 按钮点击
data-umami-event="navigation_click"
data-umami-event-region="hero_cta"
data-umami-event-label="Access Articles"
>
<button className="w-full py-3 border border-[var(--background)] font-sans text-xs uppercase tracking-widest hover:bg-[var(--background)] hover:text-[var(--foreground)] transition-all cursor-pointer">
Access Articles / 访问文章
Expand Down Expand Up @@ -113,8 +114,9 @@ export function Hero() {
<Link
href={c.href}
className="p-8 hover:bg-neutral-100 dark:hover:bg-neutral-900 transition-colors h-full flex flex-col hard-shadow-hover"
data-umami-event="home_category_click"
data-umami-event-category={c.title}
// Umami 埋点: 首页分类卡片点击
data-umami-event="navigation_click"
data-umami-event-region="home_categories" data-umami-event-label={c.title}
>
<div className="font-mono text-[10px] text-neutral-400 mb-4">
00{idx + 1}
Expand Down
60 changes: 60 additions & 0 deletions app/components/PageFeedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { useState } from "react";
import { usePathname } from "next/navigation";
import { Button } from "@/app/components/ui/button";
import { ThumbsUp, ThumbsDown } from "lucide-react";

export function PageFeedback() {
const pathname = usePathname();
const [voted, setVoted] = useState<"helpful" | "not_helpful" | null>(null);

const handleVote = (vote: "helpful" | "not_helpful") => {
if (voted) return;

if (window.umami) {
// Umami 埋点: 记录用户是否有帮助的投票
window.umami.track("feedback_submit", {
page: pathname,
vote,
});
}
setVoted(vote);
};

if (voted) {
return (
<div className="flex items-center gap-2 text-sm text-neutral-500 mt-8 py-4 border-t border-[var(--foreground)] font-serif italic">
<span>Thanks for your feedback! / 感谢您的反馈!</span>
</div>
);
}

return (
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 py-4 border-t border-[var(--foreground)] mt-8">
<span className="text-sm font-medium text-[var(--foreground)] font-serif">
Was this page helpful? / 这篇文章有帮助吗?
</span>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleVote("helpful")}
className="gap-2 border-[var(--foreground)] text-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-colors rounded-none font-sans"
>
<ThumbsUp className="h-4 w-4" />
Yes
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleVote("not_helpful")}
className="gap-2 border-[var(--foreground)] text-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-colors rounded-none font-sans"
>
<ThumbsDown className="h-4 w-4" />
No
</Button>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions app/docs/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { Contributors } from "@/app/components/Contributors";
import { DocsAssistant } from "@/app/components/DocsAssistant";
import { LicenseNotice } from "@/app/components/LicenseNotice";
import { PageFeedback } from "@/app/components/PageFeedback";
import fs from "fs/promises";
import path from "path";

Expand Down Expand Up @@ -92,6 +93,7 @@ export default async function DocPage({ params }: Param) {
</div>
<Mdx components={getMDXComponents()} />
<Contributors entry={contributorsEntry} />
<PageFeedback />
<section className="mt-16">
<GiscusComments docId={docIdFromPage ?? null} />
</section>
Expand Down
2 changes: 2 additions & 0 deletions app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { baseOptions } from "@/lib/layout.shared";
import type { ReactNode } from "react";
import { DocsRouteFlag } from "@/app/components/RouteFlags";
import type { PageTree } from "fumadocs-core/server";
import { CopyTracking } from "@/app/components/CopyTracking";

function pruneEmptyFolders(root: PageTree.Root): PageTree.Root {
const transformNode = (node: PageTree.Node): PageTree.Node | null => {
Expand Down Expand Up @@ -68,6 +69,7 @@ export default async function Layout({ children }: { children: ReactNode }) {
return (
<>
{/* Add a class on <html> while in docs to adjust global backgrounds */}
<CopyTracking />
<DocsRouteFlag />
<DocsLayout
tree={tree}
Expand Down
6 changes: 5 additions & 1 deletion app/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ export default function NotFound() {
const pathname = usePathname();

useEffect(() => {
// Umami 埋点: 记录 404 错误和来源页面 (Referrer)
if (window.umami) {
window.umami.track("404", { path: pathname });
window.umami.track("error_404", {
path: pathname,
referrer: document.referrer || "direct",
});
}
}, [pathname]);

Expand Down
Loading
Loading