diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/[domain]/browse/[...path]/page.tsx index 84c87912..9c9b19e7 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/page.tsx @@ -3,6 +3,33 @@ import { getBrowseParamsFromPathParam } from "../hooks/utils"; import { CodePreviewPanel } from "./components/codePreviewPanel"; import { Loader2 } from "lucide-react"; import { TreePreviewPanel } from "./components/treePreviewPanel"; +import { Metadata } from "next"; +import { parsePathForTitle} from "@/lib/utils"; + +type Props = { + params: Promise<{ + domain: string; + path: string[]; + }>; +}; + +export async function generateMetadata({ params: paramsPromise }: Props): Promise { + let title = 'Browse'; // Default Fallback + + try { + const params = await paramsPromise; + title = parsePathForTitle(params.path); + + } catch (error) { + // TODO: Maybe I need to look into a better way of handling this error. + // for now, it is just a log, fallback tab title and prevents the app from crashing. + console.error("Failed to generate metadata title from path:", error); + } + + return { + title, + }; +} interface BrowsePageProps { params: Promise<{ diff --git a/packages/web/src/app/layout.tsx b/packages/web/src/app/layout.tsx index dc0d4b8c..441e5e58 100644 --- a/packages/web/src/app/layout.tsx +++ b/packages/web/src/app/layout.tsx @@ -11,9 +11,15 @@ import { PlanProvider } from "@/features/entitlements/planProvider"; import { getEntitlements } from "@sourcebot/shared"; export const metadata: Metadata = { - title: "Sourcebot", - description: "Sourcebot is a self-hosted code understanding tool. Ask questions about your codebase and get rich Markdown answers with inline citations.", - manifest: "/manifest.json", + // Using the title.template will allow child pages to set the title + // while keeping a consistent suffix. + title: { + default: "Sourcebot", + template: "%s | Sourcebot", + }, + description: + "Sourcebot is a self-hosted code understanding tool. Ask questions about your codebase and get rich Markdown answers with inline citations.", + manifest: "/manifest.json", }; export default function RootLayout({ diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index 38b52cfe..261c9270 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -17,6 +17,7 @@ import { ErrorCode } from "./errorCodes"; import { NextRequest } from "next/server"; import { Org } from "@sourcebot/db"; import { OrgMetadata, orgMetadataSchema } from "@/types"; +import { getBrowseParamsFromPathParam } from "@/app/[domain]/browse/hooks/utils"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -486,4 +487,44 @@ export const isHttpError = (error: unknown, status: number): boolean => { && typeof error === 'object' && 'status' in error && error.status === status; +} + +/** + * Parses the URL path to generate a descriptive title. + * It handles three cases: + * 1. File view (`blob`): "filename.ts - owner/repo" + * 2. Directory view (`tree`): "directory/ - owner/repo" + * 3. Repository root: "owner/repo" + * + * @param path The array of path segments from Next.js params. + * @returns A formatted title string. + */ +export const parsePathForTitle = (path: string[]): string => { + const pathParam = path.join('/'); + + const { repoName, revisionName, path: filePath, pathType } = getBrowseParamsFromPathParam(pathParam); + + // Build the base repository and revision string. + const cleanRepoName = repoName.split('/').slice(1).join('/'); // Remove the version control system prefix + const repoAndRevision = `${cleanRepoName}${revisionName ? ` @ ${revisionName}` : ''}`; + + switch (pathType) { + case 'blob': { + // For blobs, get the filename from the end of the path. + const fileName = filePath.split('/').pop() || filePath; + return `${fileName} - ${repoAndRevision}`; + } + case 'tree': { + // If the path is empty, it's the repo root. + if (filePath === '' || filePath === '/') { + return repoAndRevision; + } + // Otherwise, show the directory path. + const directoryPath = filePath.endsWith('/') ? filePath : `${filePath}/`; + return `${directoryPath} - ${repoAndRevision}`; + } + default: + // Fallback to just the repository name. + return repoAndRevision; + } } \ No newline at end of file