diff --git a/.env.example b/.env.example
index c149708..f62e0a2 100644
--- a/.env.example
+++ b/.env.example
@@ -28,5 +28,8 @@ DB_DISABLE_SSL="true"
RESEND_API_KEY="your-resend-api-key"
RESEND_DOMAIN=""
+# Analytics
+NEXT_PUBLIC_UMAMI_WEBSITE_ID="your-website-id-here"
+NEXT_PUBLIC_UMAMI_SCRIPT_URL="https://analytics.umami.is/script.js"
# Build tweaks
# SKIP_ENV_VALIDATION=1
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 3381454..b524e3f 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -9,6 +9,7 @@ import { AnimationPreferencesProvider } from "~/components/providers/animation-p
import { ThemeProvider } from "~/components/providers/theme-provider";
import { ColorThemeProvider } from "~/components/providers/color-theme-provider";
+import { UmamiScript } from "~/components/analytics/umami-script";
export const metadata: Metadata = {
title: "beenvoice - Invoicing Made Simple",
@@ -143,6 +144,7 @@ export default function RootLayout({
{children}
+
diff --git a/src/components/analytics/umami-script.tsx b/src/components/analytics/umami-script.tsx
new file mode 100644
index 0000000..8ba8e3e
--- /dev/null
+++ b/src/components/analytics/umami-script.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import Script from "next/script";
+import { env } from "~/env";
+
+export function UmamiScript() {
+ if (!env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || !env.NEXT_PUBLIC_UMAMI_SCRIPT_URL) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/env.js b/src/env.js
index 52831d5..17358fa 100644
--- a/src/env.js
+++ b/src/env.js
@@ -31,6 +31,8 @@ export const env = createEnv({
*/
client: {
NEXT_PUBLIC_APP_URL: z.string().url().optional(),
+ NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
+ NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.string().url().optional(),
},
/**
@@ -46,6 +48,8 @@ export const env = createEnv({
NODE_ENV: process.env.NODE_ENV,
DB_DISABLE_SSL: process.env.DB_DISABLE_SSL,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
+ NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
+ NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
diff --git a/src/server/umami.ts b/src/server/umami.ts
new file mode 100644
index 0000000..77ae54d
--- /dev/null
+++ b/src/server/umami.ts
@@ -0,0 +1,65 @@
+import { env } from "~/env";
+
+type UmamiPayload = {
+ payload: {
+ hostname: string;
+ language: string;
+ referrer: string;
+ screen: string;
+ title: string;
+ url: string;
+ website: string;
+ name: string;
+ data?: Record;
+ };
+ type: "event";
+};
+
+export async function trackServerEvent(
+ eventName: string,
+ eventData?: Record,
+) {
+ if (!env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || !env.NEXT_PUBLIC_UMAMI_SCRIPT_URL) {
+ console.warn("Umami not configured, skipping server-side event tracking");
+ return;
+ }
+
+ // Extract API endpoint from script URL (e.g., https://analytics.umami.is/script.js -> https://analytics.umami.is/api/send)
+ const scriptUrl = new URL(env.NEXT_PUBLIC_UMAMI_SCRIPT_URL);
+ const apiUrl = `${scriptUrl.origin}/api/send`;
+
+ const payload: UmamiPayload = {
+ payload: {
+ hostname: env.NEXT_PUBLIC_APP_URL
+ ? new URL(env.NEXT_PUBLIC_APP_URL).hostname
+ : "localhost",
+ language: "en-US",
+ referrer: "",
+ screen: "",
+ title: "Server Event",
+ url: "/",
+ website: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
+ name: eventName,
+ data: eventData,
+ },
+ type: "event",
+ };
+
+ try {
+ const response = await fetch(apiUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "User-Agent":
+ "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
+ },
+ body: JSON.stringify(payload),
+ });
+
+ if (!response.ok) {
+ console.error("Failed to send Umami event:", await response.text());
+ }
+ } catch (error) {
+ console.error("Error sending Umami event:", error);
+ }
+}