Skip to content

Commit bb66666

Browse files
committed
feat(metadata): Enhance metadata generation for repository browsing
feat(utils): Add parseRepoPath function to extract repository name and revision from URL path
1 parent c3fae1a commit bb66666

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

packages/web/src/app/[domain]/browse/[...path]/page.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,36 @@ import { getBrowseParamsFromPathParam } from "../hooks/utils";
33
import { CodePreviewPanel } from "./components/codePreviewPanel";
44
import { Loader2 } from "lucide-react";
55
import { TreePreviewPanel } from "./components/treePreviewPanel";
6+
import { Metadata } from "next";
7+
import { parseRepoPath } from "@/lib/utils";
8+
9+
type Props = {
10+
params: {
11+
domain: string;
12+
path: string[];
13+
};
14+
};
15+
16+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
17+
let title = 'Browse'; // Current Default
18+
19+
try {
20+
const parsedInfo = parseRepoPath(params.path);
21+
22+
if (parsedInfo) {
23+
const { fullRepoName, revision } = parsedInfo;
24+
title = `${fullRepoName}${revision ? ` @ ${revision}` : ''}`;
25+
}
26+
} catch (error) {
27+
// Log the error for debugging, but don't crash the page render.
28+
console.error("Failed to generate metadata title from path:", params.path, error);
29+
}
30+
31+
return {
32+
title, // e.g., "sourcebot-dev/sourcebot @ HEAD"
33+
};
34+
}
35+
636

737
interface BrowsePageProps {
838
params: Promise<{

packages/web/src/app/layout.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ import { PlanProvider } from "@/features/entitlements/planProvider";
1111
import { getEntitlements } from "@sourcebot/shared";
1212

1313
export const metadata: Metadata = {
14-
title: "Sourcebot",
15-
description: "Sourcebot is a self-hosted code understanding tool. Ask questions about your codebase and get rich Markdown answers with inline citations.",
16-
manifest: "/manifest.json",
14+
// Using the title.template will allow child pages to set the title
15+
// while keeping a consistent suffix.
16+
title: {
17+
default: "Sourcebot",
18+
template: "%s | Sourcebot",
19+
},
20+
description:
21+
"Sourcebot is a self-hosted code understanding tool. Ask questions about your codebase and get rich Markdown answers with inline citations.",
22+
manifest: "/manifest.json",
1723
};
1824

1925
export default function RootLayout({

packages/web/src/lib/utils.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,4 +486,56 @@ export const isHttpError = (error: unknown, status: number): boolean => {
486486
&& typeof error === 'object'
487487
&& 'status' in error
488488
&& error.status === status;
489+
}
490+
491+
492+
/**
493+
* Parses a URL path array to extract the full repository name and revision.
494+
* This function assumes a URL structure like:
495+
* `.../[hostname]/[owner]/[repo@revision]/-/tree/...`
496+
* Or for nested groups (like GitLab):
497+
* `.../[hostname]/[group]/[subgroup]/[repo@revision]/-/tree/...`
498+
*
499+
* @param path The array of path segments from Next.js params.
500+
* @returns An object with fullRepoName and revision, or null if parsing fails.
501+
*/
502+
export const parseRepoPath = (path: string[]): { fullRepoName: string; revision: string } | null => {
503+
if (path.length < 2) {
504+
return null; // Not enough path segments to parse.
505+
}
506+
507+
// Find the index of the `-` delimiter which separates the repo info from the file tree info.
508+
const delimiterIndex = path.indexOf('-');
509+
510+
// If no delimiter is found, we can't reliably parse the path.
511+
if (delimiterIndex === -1) {
512+
return null;
513+
}
514+
515+
// The repository parts are between the hostname (index 0) and the delimiter.
516+
// e.g., ["github.com", "sourcebot-dev", "sourcebot"] -> slice will be ["sourcebot-dev", "sourcebot"]
517+
const repoParts = path.slice(1, delimiterIndex);
518+
519+
if (repoParts.length === 0) {
520+
return null;
521+
}
522+
523+
// The last part of the repo segment potentially contains the revision.
524+
const lastPart = repoParts[repoParts.length - 1];
525+
526+
// URL segments are encoded. Decode it to handle characters like '@' (%40).
527+
const decodedLastPart = decodeURIComponent(lastPart);
528+
529+
const [repoNamePart, revision = ''] = decodedLastPart.split('@');
530+
531+
// The preceding parts form the owner/group path.
532+
// e.g., ["sourcebot"] or ["my-group", "my-subgroup"]
533+
const ownerParts = repoParts.slice(0, repoParts.length - 1);
534+
535+
// Reconstruct the full repository name.
536+
// e.g., "sourcebot-dev" + "/" + "sourcebot"
537+
// e.g., "my-group/my-subgroup" + "/" + "my-repo"
538+
const fullRepoName = [...ownerParts, repoNamePart].join('/');
539+
540+
return { fullRepoName, revision };
489541
}

0 commit comments

Comments
 (0)