feat: switch site analytics to umami

This commit is contained in:
2026-04-23 11:26:59 +08:00
parent 54ff5ba89d
commit 3e368e663e
6 changed files with 57 additions and 95 deletions
+5
View File
@@ -10,3 +10,8 @@ N8N_AI_SEARCH_WEBHOOK="https://n8n.mzaxd.fun/webhook/ai-search"
# Internationalization
NEXT_INTL_DEFAULT_LOCALE="zh"
NEXT_INTL_SUPPORTED_LOCALES="zh,en"
# Optional self-hosted analytics
NEXT_PUBLIC_UMAMI_HOST_URL="https://analytics.example.com"
NEXT_PUBLIC_UMAMI_WEBSITE_ID="your-umami-website-id"
NEXT_PUBLIC_UMAMI_DOMAINS="example.com,www.example.com"
-2
View File
@@ -14,8 +14,6 @@
},
"dependencies": {
"@prisma/client": "^6.1.0",
"@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1",
"lucide-react": "^0.468.0",
"next": "15.1.11",
"next-intl": "^4.0.2",
-65
View File
@@ -11,12 +11,6 @@ importers:
'@prisma/client':
specifier: ^6.1.0
version: 6.19.1(prisma@6.19.1(typescript@5.9.3))(typescript@5.9.3)
'@vercel/analytics':
specifier: ^1.6.1
version: 1.6.1(next@15.1.11(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
'@vercel/speed-insights':
specifier: ^1.3.1
version: 1.3.1(next@15.1.11(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
lucide-react:
specifier: ^0.468.0
version: 0.468.0(react@19.2.3)
@@ -1044,55 +1038,6 @@ packages:
cpu: [x64]
os: [win32]
'@vercel/analytics@1.6.1':
resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==}
peerDependencies:
'@remix-run/react': ^2
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
react: ^18 || ^19 || ^19.0.0-rc
svelte: '>= 4'
vue: ^3
vue-router: ^4
peerDependenciesMeta:
'@remix-run/react':
optional: true
'@sveltejs/kit':
optional: true
next:
optional: true
react:
optional: true
svelte:
optional: true
vue:
optional: true
vue-router:
optional: true
'@vercel/speed-insights@1.3.1':
resolution: {integrity: sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==}
peerDependencies:
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
react: ^18 || ^19 || ^19.0.0-rc
svelte: '>= 4'
vue: ^3
vue-router: ^4
peerDependenciesMeta:
'@sveltejs/kit':
optional: true
next:
optional: true
react:
optional: true
svelte:
optional: true
vue:
optional: true
vue-router:
optional: true
'@vitest/expect@2.1.9':
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
@@ -3797,16 +3742,6 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
'@vercel/analytics@1.6.1(next@15.1.11(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)':
optionalDependencies:
next: 15.1.11(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
'@vercel/speed-insights@1.3.1(next@15.1.11(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)':
optionalDependencies:
next: 15.1.11(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3
'@vitest/expect@2.1.9':
dependencies:
'@vitest/spy': 2.1.9
+50
View File
@@ -0,0 +1,50 @@
import Script from "next/script";
const beforeSendScript = `
window.umamiBeforeSend = function (_type, payload) {
try {
var rawUrl = payload && typeof payload.url === "string" ? payload.url : window.location.href;
var pathname = new URL(rawUrl, window.location.origin).pathname;
var isTrackable =
pathname === "/zh" ||
pathname === "/en" ||
pathname.startsWith("/zh/") ||
pathname.startsWith("/en/");
return isTrackable ? payload : false;
} catch (_error) {
return false;
}
};
`;
export function UmamiMetrics() {
const hostUrl = process.env.NEXT_PUBLIC_UMAMI_HOST_URL?.replace(/\/+$/, "");
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
const domains = process.env.NEXT_PUBLIC_UMAMI_DOMAINS;
if (!hostUrl || !websiteId) {
return null;
}
return (
<>
<Script id="umami-before-send" strategy="beforeInteractive">
{beforeSendScript}
</Script>
<Script
id="umami-script"
src={`${hostUrl}/script.js`}
strategy="afterInteractive"
data-website-id={websiteId}
data-host-url={hostUrl}
data-domains={domains || undefined}
data-before-send="umamiBeforeSend"
data-exclude-search="true"
data-exclude-hash="true"
data-do-not-track="true"
data-performance="true"
/>
</>
);
}
-24
View File
@@ -1,24 +0,0 @@
"use client"
import { Analytics } from "@vercel/analytics/next"
import { SpeedInsights } from "@vercel/speed-insights/next"
const isTrackablePath = (url: string): boolean => {
try {
const pathname = new URL(url).pathname
// Only keep user-facing locale pages to control usage on the Hobby plan.
return pathname === "/zh" || pathname === "/en" || pathname.startsWith("/zh/") || pathname.startsWith("/en/")
} catch {
return false
}
}
export function VercelMetrics() {
return (
<>
<Analytics mode="production" beforeSend={(event) => (isTrackablePath(event.url) ? event : null)} />
<SpeedInsights sampleRate={0.2} beforeSend={(data) => (isTrackablePath(data.url) ? data : null)} />
</>
)
}
+2 -4
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next"
import { VercelMetrics } from "./VercelMetrics"
import { UmamiMetrics } from "./UmamiMetrics"
import "./globals.css"
export const metadata: Metadata = {
@@ -7,8 +7,6 @@ export const metadata: Metadata = {
description: "发现和探索全网优质 AI 项目",
}
const isVercelProduction = process.env.VERCEL_ENV === "production"
export default function RootLayout({
children,
}: Readonly<{
@@ -18,7 +16,7 @@ export default function RootLayout({
<html lang="zh" suppressHydrationWarning>
<body className="font-sans antialiased">
{children}
{isVercelProduction ? <VercelMetrics /> : null}
<UmamiMetrics />
</body>
</html>
)