-
- {title
- ? `${title} | ${titleSuffix}`
- : selectedTable
- ? `${selectedTable} | ${projectName} | ${organizationName} | ${titleSuffix}`
- : projectName
- ? `${projectName} | ${organizationName} | ${titleSuffix}`
- : organizationName
- ? `${organizationName} | ${titleSuffix}`
- : titleSuffix}
-
+ {pageTitle}
diff --git a/apps/studio/lib/page-title.test.ts b/apps/studio/lib/page-title.test.ts
new file mode 100644
index 00000000000..a05f31c32c2
--- /dev/null
+++ b/apps/studio/lib/page-title.test.ts
@@ -0,0 +1,94 @@
+import { describe, expect, it } from 'vitest'
+
+import { buildStudioPageTitle, STUDIO_PAGE_TITLE_SEPARATOR } from './page-title'
+
+describe('buildStudioPageTitle', () => {
+ it('builds a project-scoped title in most-specific-first order', () => {
+ expect(
+ buildStudioPageTitle({
+ surface: 'Database',
+ project: 'Acme Project',
+ org: 'Acme Org',
+ brand: 'Supabase',
+ })
+ ).toBe(
+ `Database${STUDIO_PAGE_TITLE_SEPARATOR}Acme Project${STUDIO_PAGE_TITLE_SEPARATOR}Acme Org${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
+ )
+ })
+
+ it('includes entity and section when provided', () => {
+ expect(
+ buildStudioPageTitle({
+ entity: 'users',
+ section: 'Tables',
+ surface: 'Database',
+ project: 'Acme Project',
+ org: 'Acme Org',
+ brand: 'Supabase',
+ })
+ ).toBe(
+ `users${STUDIO_PAGE_TITLE_SEPARATOR}Tables${STUDIO_PAGE_TITLE_SEPARATOR}Database${STUDIO_PAGE_TITLE_SEPARATOR}Acme Project${STUDIO_PAGE_TITLE_SEPARATOR}Acme Org${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
+ )
+ })
+
+ it('omits missing segments', () => {
+ expect(
+ buildStudioPageTitle({
+ section: 'Authentication',
+ project: 'Acme Project',
+ brand: 'Supabase',
+ })
+ ).toBe(
+ `Authentication${STUDIO_PAGE_TITLE_SEPARATOR}Acme Project${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
+ )
+ })
+
+ it('deduplicates adjacent segments case-insensitively', () => {
+ expect(
+ buildStudioPageTitle({
+ section: 'Database',
+ surface: 'database',
+ project: 'Acme Project',
+ org: 'Acme Org',
+ brand: 'Supabase',
+ })
+ ).toBe(
+ `Database${STUDIO_PAGE_TITLE_SEPARATOR}Acme Project${STUDIO_PAGE_TITLE_SEPARATOR}Acme Org${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
+ )
+ })
+
+ it('normalizes whitespace in each segment', () => {
+ expect(
+ buildStudioPageTitle({
+ entity: ' hello world ',
+ surface: ' Edge Functions ',
+ brand: ' Supabase ',
+ })
+ ).toBe(
+ `hello world${STUDIO_PAGE_TITLE_SEPARATOR}Edge Functions${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
+ )
+ })
+
+ it('truncates very long segments', () => {
+ const longName = 'x'.repeat(80)
+
+ expect(
+ buildStudioPageTitle({
+ entity: longName,
+ surface: 'Table Editor',
+ brand: 'Supabase',
+ })
+ ).toBe(
+ `${'x'.repeat(59)}…${STUDIO_PAGE_TITLE_SEPARATOR}Table Editor${STUDIO_PAGE_TITLE_SEPARATOR}Supabase`
+ )
+ })
+
+ it('supports custom brand titles', () => {
+ expect(
+ buildStudioPageTitle({
+ surface: 'Settings',
+ brand: 'Supabase Studio',
+ })
+ ).toBe(`Settings${STUDIO_PAGE_TITLE_SEPARATOR}Supabase Studio`)
+ })
+})
diff --git a/apps/studio/lib/page-title.ts b/apps/studio/lib/page-title.ts
new file mode 100644
index 00000000000..30918e48b38
--- /dev/null
+++ b/apps/studio/lib/page-title.ts
@@ -0,0 +1,46 @@
+export interface StudioPageTitleParts {
+ entity?: string
+ section?: string
+ surface?: string
+ project?: string
+ org?: string
+ brand?: string
+}
+
+export const STUDIO_PAGE_TITLE_SEPARATOR = ' | '
+const MAX_SEGMENT_LENGTH = 60
+
+const normalizeTitleSegment = (value?: string) => {
+ if (value === undefined) return undefined
+
+ const normalized = value.trim().replace(/\s+/g, ' ')
+ if (normalized.length === 0) return undefined
+
+ if (normalized.length <= MAX_SEGMENT_LENGTH) return normalized
+ return `${normalized.slice(0, MAX_SEGMENT_LENGTH - 1).trimEnd()}…`
+}
+
+export const buildStudioPageTitle = (parts: StudioPageTitleParts) => {
+ const orderedParts = [
+ parts.entity,
+ parts.section,
+ parts.surface,
+ parts.project,
+ parts.org,
+ parts.brand,
+ ]
+
+ const segments: string[] = []
+
+ orderedParts.forEach((part) => {
+ const segment = normalizeTitleSegment(part)
+ if (!segment) return
+
+ const lastSegment = segments[segments.length - 1]
+ if (lastSegment !== undefined && lastSegment.toLowerCase() === segment.toLowerCase()) return
+
+ segments.push(segment)
+ })
+
+ return segments.join(STUDIO_PAGE_TITLE_SEPARATOR)
+}