Skip to content

Commit a9d65ca

Browse files
TMHSDigitalclaude
andcommitted
Per-entry licence badge and "Link to this entry" snippets (part of #117)
- lib/badge.mjs: badge/<id>.svg per entry, "Free Game Dev Assets | <licence> · read <date>", coloured by licence family, with an accessible name - each entry page has a "Link to this entry" box with the badge as Markdown and HTML - docs/for-publishers.md explains the badge and the new per-entry licence-change feed Not done: unique per-entry social cards. The build no longer has a PNG writer (the site card is a screenshot), and drawing text into PNGs with no dependencies would look worse than the shared card. #117 stays open for that. Refs #117 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpkeXQPaUhxjgqdqnk6BrJ
1 parent cd6865d commit a9d65ca

5 files changed

Lines changed: 92 additions & 0 deletions

File tree

‎docs/for-publishers.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,15 @@ https://tmhsdigital.github.io/Free-Game-Dev-Assets/entry/<id>/
4545
```
4646

4747
The `<id>` is the entry's file name, for example `kenney` for `catalog/3d/kenney.md`.
48+
49+
Each entry also has a badge saying when the catalog last read your licence, coloured by licence family:
50+
51+
```text
52+
https://tmhsdigital.github.io/Free-Game-Dev-Assets/badge/<id>.svg
53+
```
54+
55+
The entry page's "Link to this entry" box has it as Markdown and HTML, ready to paste into a README or store page. The date updates whenever the licence is re-read.
56+
57+
## Hear about changes to your entry
58+
59+
Each entry page links an Atom feed, `entry/<id>/changes.xml`, with an item whenever the recorded licence, commercial use, credit requirement or status changes.

‎site/build.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { createDistWriter } from "./lib/dist-writer.mjs";
4040
import { serviceWorkerJs, webManifest } from "./lib/offline.mjs";
4141
import { auditPageHtml } from "./lib/audit-page.mjs";
4242
import { changeEventsFromLog, changesFeedXml } from "./lib/changes.mjs";
43+
import { badgeSvg } from "./lib/badge.mjs";
4344
import { ATTRIBUTION_RANK, categoryChipsHtml, guideRowsHtml, headMetaHtml, licenseFamily, notFoundHtml, robotsTxt, stackRowsHtml, starterRowsHtml } from "./lib/home-page.mjs";
4445

4546
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -599,6 +600,8 @@ function main() {
599600
// One licence-change feed per entry, beside its page (#186).
600601
const changes = licenceChanges();
601602
for (const entry of entries) {
603+
// A badge publishers can link back with (#117).
604+
writeOut(path.join("badge", `${entry.id}.svg`), badgeSvg(entry));
602605
writeOut(path.join("entry", entry.id, "changes.xml"), changesFeedXml({ entry, events: changes.get(entry.id) || [], siteUrl: config.site.siteUrl, generatedAt: payload.generatedAt }));
603606
}
604607
const stackPages = writeStackPages({ stacks, entries, config, stats, stamp, hasCard, now });

‎site/lib/badge.mjs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* The per-entry badge (#117): a static SVG at badge/<id>.svg saying the
3+
* catalog read the source's licence and when, for publishers to link back
4+
* with. Coloured by licence family. Pure.
5+
*/
6+
import { esc } from "./shared.mjs";
7+
8+
const FAMILY_COLOURS = {
9+
"public-domain": "#2e7d32",
10+
permissive: "#1565c0",
11+
"cc-by": "#00796b",
12+
copyleft: "#b26a00",
13+
"non-commercial": "#8b2e2e",
14+
};
15+
16+
/** Approximate text width at 11px Verdana, as shields.io badges measure it. */
17+
export function textWidth(text) {
18+
let w = 0;
19+
for (const ch of String(text)) w += /[mwMW@]/.test(ch) ? 10 : /[ijlI.,:;'|!·]/.test(ch) ? 3.5 : /[A-Z0-9]/.test(ch) ? 7.5 : 6.5;
20+
return Math.ceil(w);
21+
}
22+
23+
/** The badge SVG: "Free Game Dev Assets | <licence> · read <date>". */
24+
export function badgeSvg(entry) {
25+
const left = "Free Game Dev Assets";
26+
const right = `${entry.license}${entry.verified ? ` · read ${entry.verified}` : ""}`;
27+
const lw = textWidth(left) + 12;
28+
const rw = textWidth(right) + 12;
29+
const colour = FAMILY_COLOURS[entry.licenseFamily] || "#5a6472";
30+
const label = `${left}: ${right}`;
31+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${lw + rw}" height="20" role="img" aria-label="${esc(label)}">
32+
<title>${esc(label)}</title>
33+
<rect width="${lw}" height="20" fill="#12151a"/>
34+
<rect x="${lw}" width="${rw}" height="20" fill="${colour}"/>
35+
<g fill="#fff" font-family="Verdana,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle">
36+
<text x="${lw / 2}" y="14">${esc(left)}</text>
37+
<text x="${lw + rw / 2}" y="14">${esc(right)}</text>
38+
</g>
39+
</svg>
40+
`;
41+
}
42+
43+
/** Markdown and HTML a publisher can paste to show the badge, linking the entry page. */
44+
export function badgeSnippets(entry, siteUrl) {
45+
const base = String(siteUrl).replace(/\/+$/, "");
46+
const page = `${base}/entry/${entry.id}/`;
47+
const img = `${base}/badge/${entry.id}.svg`;
48+
const alt = `Licence read by Free Game Dev Assets`;
49+
return {
50+
markdown: `[![${alt}](${img})](${page})`,
51+
html: `<a href="${page}"><img src="${img}" alt="${alt}" /></a>`,
52+
};
53+
}

‎site/lib/entry-page.mjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { thumbHtml } from "./cards.mjs";
2020
import { previewCredit } from "./thumbs.mjs";
2121
import { creditLineHtml, fillInNote } from "./owes.mjs";
2222
import { ENGINES, nativeEngines } from "./search.mjs";
23+
import { badgeSnippets } from "./badge.mjs";
2324

2425
export function jsonLd(entry, site, { categoryLabel } = {}) {
2526
const work = { "@type": "CreativeWork", name: entry.name, url: entry.url };
@@ -175,6 +176,14 @@ export function entryPageHtml({ entry, leadHtml, restHtml, deprecatedReasonHtml,
175176
${c.block}
176177
</section>
177178
<p class="page-actions"><a class="btn" href="${esc(entry.url)}" rel="noopener noreferrer">Go to source</a></p>
179+
<details class="entry-badge">
180+
<summary>Link to this entry</summary>
181+
<p><img src="../../badge/${esc(entry.id)}.svg" alt="Licence read by Free Game Dev Assets" /></p>
182+
<p>Markdown:</p>
183+
<pre class="attribution-string"><code>${esc(badgeSnippets(entry, site.siteUrl).markdown)}</code></pre>
184+
<p>HTML:</p>
185+
<pre class="attribution-string"><code>${esc(badgeSnippets(entry, site.siteUrl).html)}</code></pre>
186+
</details>
178187
<p class="entry-feed"><a href="changes.xml" type="application/atom+xml">Follow licence changes</a> (an Atom feed of changes to this licence, its commercial use, credit or status)</p>
179188
<p class="page-links">
180189
<a href="${esc(`${repo}/blob/main/${entry.path}`)}" rel="noopener noreferrer">View the file on GitHub</a>

‎site/lib/lib.test.mjs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2670,6 +2670,21 @@ await section("licence-change feeds (#186)", async () => {
26702670
has("opml: names escaped", opml, 'text="A &lt;1&gt;"');
26712671
});
26722672

2673+
/* entry badge (#117) ------------------------------------------------------ */
2674+
await section("entry badge (#117)", async () => {
2675+
const { badgeSvg, badgeSnippets, textWidth } = await import("./badge.mjs");
2676+
const svg = badgeSvg({ id: "a", license: "CC-BY-4.0", licenseFamily: "cc-by", verified: "2026-10-13" });
2677+
has("badge: licence and read date", svg, "CC-BY-4.0 · read 2026-10-13");
2678+
has("badge: accessible name", svg, 'aria-label="Free Game Dev Assets: CC-BY-4.0 · read 2026-10-13"');
2679+
has("badge: coloured by family", svg, 'fill="#00796b"');
2680+
has("badge: unknown family is grey", badgeSvg({ id: "b", license: "custom", licenseFamily: "custom", verified: null }), 'fill="#5a6472"');
2681+
has("badge: escaped", badgeSvg({ id: "c", license: "A<&>", verified: null }), "A&lt;&amp;&gt;");
2682+
eq("badge: wider text, wider badge", textWidth("MMMM") > textWidth("iiii"), true);
2683+
const s = badgeSnippets({ id: "a" }, "https://x.test/s/");
2684+
eq("badge: markdown links the entry", s.markdown, "[![Licence read by Free Game Dev Assets](https://x.test/s/badge/a.svg)](https://x.test/s/entry/a/)");
2685+
has("badge: html too", s.html, '<img src="https://x.test/s/badge/a.svg"');
2686+
});
2687+
26732688
/* report (keep last) ------------------------------------------------------ */
26742689
if (failures.length) {
26752690
console.error(`lib.test failed (${failures.length}):`);

0 commit comments

Comments
 (0)