diff --git a/.gitignore b/.gitignore
index 753389d1..64cdb30d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@ package-lock.json
*.log
.vscode
.tool-versions
+.claude
# debug
npm-debug.log*
diff --git a/package.json b/package.json
index 09a69135..2c0279aa 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
},
"type": "module",
"scripts": {
- "dev": "next dev -p 3001 --turbo",
+ "dev": "next dev -p 3003 --turbo",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
index 7dd1d771..0d8b5d28 100644
--- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
+++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx
@@ -8,7 +8,13 @@ import { Edit, Share } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
-export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
+export function WebsiteHeader({
+ showActions,
+ allowLink = true,
+}: {
+ showActions?: boolean;
+ allowLink?: boolean;
+}) {
const website = useWebsite();
const { renderUrl, pathname } = useNavigation();
const isSettings = pathname.endsWith('/settings');
@@ -23,7 +29,7 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
}
- titleHref={renderUrl(`/websites/${website.id}`, false)}
+ titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined}
>
diff --git a/src/app/api/share/[shareId]/route.ts b/src/app/api/share/[shareId]/route.ts
index bef87c4f..8b0c9bb9 100644
--- a/src/app/api/share/[shareId]/route.ts
+++ b/src/app/api/share/[shareId]/route.ts
@@ -1,8 +1,53 @@
+import { ROLES } from '@/lib/constants';
import { secret } from '@/lib/crypto';
import { createToken } from '@/lib/jwt';
+import prisma from '@/lib/prisma';
+import redis from '@/lib/redis';
import { json, notFound } from '@/lib/response';
import { getSharedWebsite } from '@/queries/prisma';
+export interface WhiteLabel {
+ name: string;
+ url: string;
+ image: string;
+}
+
+async function getAccountId(website: { userId?: string; teamId?: string }): Promise {
+ if (website.userId) {
+ return website.userId;
+ }
+
+ if (website.teamId) {
+ const teamOwner = await prisma.client.teamUser.findFirst({
+ where: {
+ teamId: website.teamId,
+ role: ROLES.teamOwner,
+ },
+ select: {
+ userId: true,
+ },
+ });
+
+ return teamOwner?.userId || null;
+ }
+
+ return null;
+}
+
+async function getWhiteLabel(accountId: string): Promise {
+ if (!redis.enabled) {
+ return null;
+ }
+
+ const data = await redis.client.get(`white-label:${accountId}`);
+
+ if (data) {
+ return data as WhiteLabel;
+ }
+
+ return null;
+}
+
export async function GET(_request: Request, { params }: { params: Promise<{ shareId: string }> }) {
const { shareId } = await params;
@@ -12,8 +57,20 @@ export async function GET(_request: Request, { params }: { params: Promise<{ sha
return notFound();
}
- const data = { websiteId: website.id };
- const token = createToken(data, secret());
+ const data: { websiteId: string; token: string; whiteLabel?: WhiteLabel } = {
+ websiteId: website.id,
+ token: createToken({ websiteId: website.id }, secret()),
+ };
- return json({ ...data, token });
+ const accountId = await getAccountId(website);
+
+ if (accountId) {
+ const whiteLabel = await getWhiteLabel(accountId);
+
+ if (whiteLabel) {
+ data.whiteLabel = whiteLabel;
+ }
+ }
+
+ return json(data);
}
diff --git a/src/app/share/[...shareId]/Footer.tsx b/src/app/share/[...shareId]/Footer.tsx
index f2948628..0f17187c 100644
--- a/src/app/share/[...shareId]/Footer.tsx
+++ b/src/app/share/[...shareId]/Footer.tsx
@@ -1,7 +1,18 @@
import { Row, Text } from '@umami/react-zen';
+import type { WhiteLabel } from '@/app/api/share/[shareId]/route';
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
-export function Footer() {
+export function Footer({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
+ if (whiteLabel) {
+ return (
+
+
+ {whiteLabel.name}
+
+
+ );
+ }
+
return (
diff --git a/src/app/share/[...shareId]/Header.tsx b/src/app/share/[...shareId]/Header.tsx
index d7b7dcb4..78e022af 100644
--- a/src/app/share/[...shareId]/Header.tsx
+++ b/src/app/share/[...shareId]/Header.tsx
@@ -1,17 +1,26 @@
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
+import type { WhiteLabel } from '@/app/api/share/[shareId]/route';
import { LanguageButton } from '@/components/input/LanguageButton';
import { PreferencesButton } from '@/components/input/PreferencesButton';
import { Logo } from '@/components/svg';
-export function Header() {
+export function Header({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
+ const logoUrl = whiteLabel?.url || 'https://umami.is';
+ const logoName = whiteLabel?.name || 'umami';
+ const logoImage = whiteLabel?.image;
+
return (
-
+
-
-
-
- umami
+ {logoImage ? (
+
+ ) : (
+
+
+
+ )}
+ {logoName}
diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx
index 7ed06673..7a76a29b 100644
--- a/src/app/share/[...shareId]/SharePage.tsx
+++ b/src/app/share/[...shareId]/SharePage.tsx
@@ -26,15 +26,17 @@ export function SharePage({ shareId }) {
return null;
}
+ const { whiteLabel } = shareToken;
+
return (
-
+
-
+
-
+
);