feat(website): add Open Graph and Twitter Card meta tags for social sharing

- Add og:type, og:url, og:title, og:description, og:image, og:site_name meta tags
- Add twitter:card, twitter:title, twitter:description, twitter:image meta tags
- Add canonical URL link element
- Use social-image.png for social preview image
- Update document.title dynamically when modal opens/closes
- Resolve resource titles from JSON data files instead of raw filenames
- Handle skill/hook folder path mismatches for title lookup
- Change title separator from '-' to '|' for consistency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Aaron Powell
2026-02-25 19:28:01 +11:00
parent 14cd2baf3a
commit f4c080b8bf
3 changed files with 97 additions and 2 deletions

View File

@@ -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",
};
/**
* 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 {
@@ -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();
@@ -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 = `
@@ -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 &&