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

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

View File

@@ -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";
@@ -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"

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 &&