mirror of
https://github.com/github/awesome-copilot.git
synced 2026-03-12 12:15:12 +00:00
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:
BIN
website/public/images/social-image.png
Normal file
BIN
website/public/images/social-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 425 KiB |
@@ -13,6 +13,8 @@ const {
|
|||||||
activeNav = "",
|
activeNav = "",
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
const base = import.meta.env.BASE_URL;
|
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
|
// Get git commit SHA and build date at build time
|
||||||
let commitSha = "unknown";
|
let commitSha = "unknown";
|
||||||
@@ -35,8 +37,24 @@ try {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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} />
|
<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="stylesheet" href={`${base}styles/global.css`} />
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
|
|||||||
@@ -20,6 +20,61 @@ let currentFilePath: string | null = null;
|
|||||||
let currentFileContent: string | null = null;
|
let currentFileContent: string | null = null;
|
||||||
let currentFileType: string | null = null;
|
let currentFileType: string | null = null;
|
||||||
let triggerElement: HTMLElement | 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
|
// Plugin data cache
|
||||||
interface PluginItem {
|
interface PluginItem {
|
||||||
@@ -329,9 +384,24 @@ export async function openFileModal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show modal with loading state
|
// Show modal with loading state
|
||||||
title.textContent = filePath.split("/").pop() || filePath;
|
const fallbackName = filePath.split("/").pop() || filePath;
|
||||||
|
title.textContent = fallbackName;
|
||||||
modal.classList.remove("hidden");
|
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
|
// Set focus to close button for accessibility
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
closeBtn?.focus();
|
closeBtn?.focus();
|
||||||
@@ -447,6 +517,7 @@ async function openPluginModal(
|
|||||||
|
|
||||||
// Update title
|
// Update title
|
||||||
title.textContent = plugin.name;
|
title.textContent = plugin.name;
|
||||||
|
document.title = `${plugin.name} | Awesome GitHub Copilot`;
|
||||||
|
|
||||||
// Render plugin view
|
// Render plugin view
|
||||||
modalContent.innerHTML = `
|
modalContent.innerHTML = `
|
||||||
@@ -531,6 +602,12 @@ export function closeModal(updateUrl = true): void {
|
|||||||
updateHash(null);
|
updateHash(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore original document title
|
||||||
|
if (originalDocumentTitle) {
|
||||||
|
document.title = originalDocumentTitle;
|
||||||
|
originalDocumentTitle = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Return focus to trigger element
|
// Return focus to trigger element
|
||||||
if (
|
if (
|
||||||
triggerElement &&
|
triggerElement &&
|
||||||
|
|||||||
Reference in New Issue
Block a user