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:
Ashley Wolf
2026-06-24 21:52:58 -07:00
committed by GitHub
parent f72401434f
commit e9c8e37041
15 changed files with 139 additions and 1 deletions
+26
View File
@@ -101,6 +101,25 @@ function normalizeText(value, fallback = "") {
return typeof value === "string" ? value.trim() : fallback;
}
/**
* Normalize an author value (npm string form or { name, url } object) to
* { name, url? } | null. Returns null when no usable name is present.
*/
function normalizeAuthor(value) {
if (!value) return null;
if (typeof value === "string") {
const name = value.trim();
return name ? { name } : null;
}
if (typeof value === "object") {
const name = normalizeText(value.name);
if (!name) return null;
const url = normalizeText(value.url);
return url ? { name, url } : { name };
}
return null;
}
/**
* Find the latest git-modified date for any file under a directory.
*/
@@ -1002,6 +1021,10 @@ function generateCanvasManifest(gitDates, commitSha) {
const packageJson = fs.existsSync(packageJsonPath)
? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
: {};
const canvasJsonPath = path.join(extensionDir, "canvas.json");
const canvasJson = fs.existsSync(canvasJsonPath)
? JSON.parse(fs.readFileSync(canvasJsonPath, "utf-8"))
: {};
const keywords = Array.isArray(packageJson.keywords)
? [...new Set(packageJson.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
: [];
@@ -1044,6 +1067,7 @@ function generateCanvasManifest(gitDates, commitSha) {
installUrl,
sourceUrl: null,
external: false,
author: normalizeAuthor(canvasJson.author),
keywords,
});
}
@@ -1116,6 +1140,7 @@ function generateCanvasManifest(gitDates, commitSha) {
installUrl,
sourceUrl: sourceUrl || null,
external: true,
author: normalizeAuthor(ext?.author),
keywords,
});
}
@@ -1199,6 +1224,7 @@ function writePerExtensionCanvasManifests(canvasManifestData) {
name: item.name,
description: item.description || "Canvas extension",
version: item.version || "1.0.0",
...(item.author ? { author: item.author } : {}),
keywords: Array.isArray(item.keywords)
? [...new Set(item.keywords)].sort((a, b) => a.localeCompare(b))
: [],