mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-26 01:10:20 +00:00
Add contributor attribution to canvas extension cards (#2111)
Show "by @handle" on each canvas extension card and in the details
modal, linking to the contributor's GitHub profile. Author metadata
lives in each extension's canvas.json (and external.json for external
extensions), where the rest of the canvas metadata is stored.
- Store author {name, url} in canvas.json / external.json
- Read author from canvas.json in the website data generator and emit
it to extensions.json
- Render the GitHub @handle, derived from the profile URL, as the link
text, with the contributor's name as the link title
- Escape the sanitized author URL before interpolating it into href
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import { escapeHtml, getGitHubUrl, getLastUpdatedHtml } from "../utils";
|
||||
import {
|
||||
escapeHtml,
|
||||
getGitHubHandle,
|
||||
getGitHubUrl,
|
||||
getLastUpdatedHtml,
|
||||
sanitizeUrl,
|
||||
} from "../utils";
|
||||
import { renderEmptyStateHtml, renderSharedCardHtml } from "./card-render";
|
||||
|
||||
export interface RenderableExtension {
|
||||
@@ -34,6 +40,7 @@ export interface RenderableExtension {
|
||||
installUrl?: string | null;
|
||||
sourceUrl?: string | null;
|
||||
external?: boolean;
|
||||
author?: { name: string; url?: string } | null;
|
||||
}
|
||||
|
||||
export type ExtensionSortOption = "title" | "lastUpdated";
|
||||
@@ -92,8 +99,27 @@ export function renderExtensionsHtml(items: RenderableExtension[]): string {
|
||||
</div>
|
||||
`;
|
||||
|
||||
const authorName = item.author?.name;
|
||||
const authorUrl = item.author?.url;
|
||||
const authorHandle =
|
||||
authorName && authorUrl
|
||||
? getGitHubHandle(authorUrl, authorName)
|
||||
: authorName || "";
|
||||
const authorHtml = authorName
|
||||
? `<span class="resource-tag resource-author">by ${
|
||||
authorUrl
|
||||
? `<a href="${escapeHtml(
|
||||
sanitizeUrl(authorUrl)
|
||||
)}" target="_blank" rel="noopener noreferrer" title="${escapeHtml(
|
||||
authorName
|
||||
)}">${escapeHtml(authorHandle)}</a>`
|
||||
: escapeHtml(authorName)
|
||||
}</span>`
|
||||
: "";
|
||||
|
||||
const metaHtml = `
|
||||
${item.external ? '<span class="resource-tag">External</span>' : ""}
|
||||
${authorHtml}
|
||||
${getLastUpdatedHtml(item.lastUpdated)}
|
||||
`;
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
copyToClipboard,
|
||||
fetchData,
|
||||
formatRelativeTime,
|
||||
getGitHubHandle,
|
||||
getGitHubUrl,
|
||||
getQueryParam,
|
||||
getQueryParamValues,
|
||||
sanitizeUrl,
|
||||
showToast,
|
||||
updateQueryParams,
|
||||
} from "../utils";
|
||||
@@ -178,6 +180,24 @@ function openDetailsModal(
|
||||
if (item.external) {
|
||||
metaParts.push('<span class="resource-tag">External</span>');
|
||||
}
|
||||
if (item.author?.name) {
|
||||
const authorName = item.author.name;
|
||||
const authorUrl = item.author.url;
|
||||
const authorHandle = authorUrl
|
||||
? getGitHubHandle(authorUrl, authorName)
|
||||
: authorName;
|
||||
metaParts.push(
|
||||
authorUrl
|
||||
? `<span class="resource-author">by <a href="${escapeHtml(
|
||||
sanitizeUrl(authorUrl)
|
||||
)}" target="_blank" rel="noopener noreferrer" title="${escapeHtml(
|
||||
authorName
|
||||
)}">${escapeHtml(authorHandle)}</a></span>`
|
||||
: `<span class="resource-author">by ${escapeHtml(
|
||||
authorName
|
||||
)}</span>`
|
||||
);
|
||||
}
|
||||
if (item.lastUpdated) {
|
||||
metaParts.push(
|
||||
`<span class="last-updated">Updated ${escapeHtml(
|
||||
|
||||
@@ -389,6 +389,31 @@ export function sanitizeUrl(url: string | null | undefined): string {
|
||||
return "#";
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a GitHub @handle from a profile URL
|
||||
* (e.g. "https://github.com/aaronpowell" -> "@aaronpowell").
|
||||
* Falls back to the provided value when the URL is not a github.com profile.
|
||||
*/
|
||||
export function getGitHubHandle(
|
||||
url: string | null | undefined,
|
||||
fallback = ""
|
||||
): string {
|
||||
if (!url) return fallback;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const host = parsed.hostname.replace(/^www\./, "");
|
||||
if (host === "github.com") {
|
||||
const segments = parsed.pathname.split("/").filter(Boolean);
|
||||
if (segments.length === 1) {
|
||||
return `@${segments[0]}`;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Invalid URL
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate text with ellipsis
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user