Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added website/public/images/social-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion website/src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const {
activeNav = "",
} = Astro.props;
const base = import.meta.env.BASE_URL;
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const socialImageUrl = new URL(`${base}images/social-image.png`, Astro.site);

// Get git commit SHA and build date at build time
let commitSha = "unknown";
Expand All @@ -35,8 +37,24 @@ try {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} - Awesome GitHub Copilot</title>
<title>{title} | Awesome GitHub Copilot</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />

<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalUrl.toString()} />
<meta property="og:title" content={`${title} | Awesome GitHub Copilot`} />
<meta property="og:description" content={description} />
<meta property="og:image" content={socialImageUrl.toString()} />
<meta property="og:site_name" content="Awesome GitHub Copilot" />

<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={`${title} | Awesome GitHub Copilot`} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={socialImageUrl.toString()} />

<link rel="stylesheet" href={`${base}styles/global.css`} />
<link
rel="icon"
Expand Down
79 changes: 78 additions & 1 deletion website/src/scripts/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,61 @@ let currentFilePath: string | null = null;
let currentFileContent: string | null = null;
let currentFileType: string | null = null;
let triggerElement: HTMLElement | null = null;
let originalDocumentTitle: string | null = null;

// Resource data cache for title lookups
interface ResourceItem {
title: string;
path: string;
}

interface ResourceData {
items: ResourceItem[];
}

const resourceDataCache: Record<string, ResourceData | null> = {};

const RESOURCE_TYPE_TO_JSON: Record<string, string> = {
agent: "agents.json",
instruction: "instructions.json",
skill: "skills.json",
hook: "hooks.json",
workflow: "workflows.json",
plugin: "plugins.json",
};

Comment on lines +43 to +45
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RESOURCE_TYPE_TO_JSON maps plugin -> plugins.json, but plugins.json items (generated by eng/generate-website-data.mjs) use name rather than title. As a result, resolveResourceTitle() can return undefined for plugins and briefly set title.textContent / document.title to undefined | Awesome GitHub Copilot when opening a plugin modal. Consider removing plugin from this lookup map (since openPluginModal() already sets the title), or special-casing plugins to read name (and updating the typed interface accordingly), or using a JSON source that actually contains a title field for plugins.

Suggested change
plugin: "plugins.json",
};
};

Copilot uses AI. Check for mistakes.
/**
* Look up the display title for a resource from its JSON data file
*/
async function resolveResourceTitle(
filePath: string,
type: string
): Promise<string> {
const fallback = filePath.split("/").pop() || filePath;
const jsonFile = RESOURCE_TYPE_TO_JSON[type];
if (!jsonFile) return fallback;

if (!(jsonFile in resourceDataCache)) {
resourceDataCache[jsonFile] = await fetchData<ResourceData>(jsonFile);
}

const data = resourceDataCache[jsonFile];
if (!data) return fallback;

// Try exact path match first
const item = data.items.find((i) => i.path === filePath);
if (item) return item.title;

// For skills/hooks, the modal receives the file path (e.g. skills/foo/SKILL.md)
// but JSON stores the folder path (e.g. skills/foo)
const parentPath = filePath.substring(0, filePath.lastIndexOf("/"));
if (parentPath) {
const parentItem = data.items.find((i) => i.path === parentPath);
if (parentItem) return parentItem.title;
}

return fallback;
}

// Plugin data cache
interface PluginItem {
Expand Down Expand Up @@ -329,9 +384,24 @@ export async function openFileModal(
}

// Show modal with loading state
title.textContent = filePath.split("/").pop() || filePath;
const fallbackName = filePath.split("/").pop() || filePath;
title.textContent = fallbackName;
modal.classList.remove("hidden");

// Update document title to reflect the open file
if (!originalDocumentTitle) {
originalDocumentTitle = document.title;
}
document.title = `${fallbackName} | Awesome GitHub Copilot`;

// Resolve the proper title from JSON data asynchronously
resolveResourceTitle(filePath, type).then((resolvedTitle) => {
if (currentFilePath === filePath) {
title.textContent = resolvedTitle;
document.title = `${resolvedTitle} | Awesome GitHub Copilot`;
}
});

// Set focus to close button for accessibility
setTimeout(() => {
closeBtn?.focus();
Expand Down Expand Up @@ -447,6 +517,7 @@ async function openPluginModal(

// Update title
title.textContent = plugin.name;
document.title = `${plugin.name} | Awesome GitHub Copilot`;

// Render plugin view
modalContent.innerHTML = `
Expand Down Expand Up @@ -531,6 +602,12 @@ export function closeModal(updateUrl = true): void {
updateHash(null);
}

// Restore original document title
if (originalDocumentTitle) {
document.title = originalDocumentTitle;
originalDocumentTitle = null;
}

// Return focus to trigger element
if (
triggerElement &&
Expand Down