feat(metadata): Enhance metadata generation for repository browsing

feat(utils): Add parseRepoPath function to extract repository name and revision from URL path
This commit is contained in:
Prateek Singh 2025-10-13 17:10:15 +05:30
parent c3fae1aaab
commit bb66666ced
3 changed files with 91 additions and 3 deletions

View file

@ -3,6 +3,36 @@ import { getBrowseParamsFromPathParam } from "../hooks/utils";
import { CodePreviewPanel } from "./components/codePreviewPanel"; import { CodePreviewPanel } from "./components/codePreviewPanel";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { TreePreviewPanel } from "./components/treePreviewPanel"; import { TreePreviewPanel } from "./components/treePreviewPanel";
import { Metadata } from "next";
import { parseRepoPath } from "@/lib/utils";
type Props = {
params: {
domain: string;
path: string[];
};
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
let title = 'Browse'; // Current Default
try {
const parsedInfo = parseRepoPath(params.path);
if (parsedInfo) {
const { fullRepoName, revision } = parsedInfo;
title = `${fullRepoName}${revision ? ` @ ${revision}` : ''}`;
}
} catch (error) {
// Log the error for debugging, but don't crash the page render.
console.error("Failed to generate metadata title from path:", params.path, error);
}
return {
title, // e.g., "sourcebot-dev/sourcebot @ HEAD"
};
}
interface BrowsePageProps { interface BrowsePageProps {
params: Promise<{ params: Promise<{

View file

@ -11,9 +11,15 @@ import { PlanProvider } from "@/features/entitlements/planProvider";
import { getEntitlements } from "@sourcebot/shared"; import { getEntitlements } from "@sourcebot/shared";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sourcebot", // Using the title.template will allow child pages to set the title
description: "Sourcebot is a self-hosted code understanding tool. Ask questions about your codebase and get rich Markdown answers with inline citations.", // while keeping a consistent suffix.
manifest: "/manifest.json", 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({ export default function RootLayout({

View file

@ -486,4 +486,56 @@ export const isHttpError = (error: unknown, status: number): boolean => {
&& typeof error === 'object' && typeof error === 'object'
&& 'status' in error && 'status' in error
&& error.status === status; && error.status === status;
}
/**
* Parses a URL path array to extract the full repository name and revision.
* This function assumes a URL structure like:
* `.../[hostname]/[owner]/[repo@revision]/-/tree/...`
* Or for nested groups (like GitLab):
* `.../[hostname]/[group]/[subgroup]/[repo@revision]/-/tree/...`
*
* @param path The array of path segments from Next.js params.
* @returns An object with fullRepoName and revision, or null if parsing fails.
*/
export const parseRepoPath = (path: string[]): { fullRepoName: string; revision: string } | null => {
if (path.length < 2) {
return null; // Not enough path segments to parse.
}
// Find the index of the `-` delimiter which separates the repo info from the file tree info.
const delimiterIndex = path.indexOf('-');
// If no delimiter is found, we can't reliably parse the path.
if (delimiterIndex === -1) {
return null;
}
// The repository parts are between the hostname (index 0) and the delimiter.
// e.g., ["github.com", "sourcebot-dev", "sourcebot"] -> slice will be ["sourcebot-dev", "sourcebot"]
const repoParts = path.slice(1, delimiterIndex);
if (repoParts.length === 0) {
return null;
}
// The last part of the repo segment potentially contains the revision.
const lastPart = repoParts[repoParts.length - 1];
// URL segments are encoded. Decode it to handle characters like '@' (%40).
const decodedLastPart = decodeURIComponent(lastPart);
const [repoNamePart, revision = ''] = decodedLastPart.split('@');
// The preceding parts form the owner/group path.
// e.g., ["sourcebot"] or ["my-group", "my-subgroup"]
const ownerParts = repoParts.slice(0, repoParts.length - 1);
// Reconstruct the full repository name.
// e.g., "sourcebot-dev" + "/" + "sourcebot"
// e.g., "my-group/my-subgroup" + "/" + "my-repo"
const fullRepoName = [...ownerParts, repoNamePart].join('/');
return { fullRepoName, revision };
} }