Skip to content

Commit 89e9776

Browse files
TMHSDigitalclaude
andcommitted
site: licence freshness page template and styles
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wc67NX9MM964uaVGn4bpZJ
1 parent 241af04 commit 89e9776

4 files changed

Lines changed: 227 additions & 1 deletion

File tree

‎site/lib/freshness.mjs‎

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Buckets come from verifiedAge, the function the homepage cards use, so the
44
* summary and the cards cannot disagree. Pure: `now` is passed in.
55
*/
6-
import { esc, verifiedAge } from "./shared.mjs";
6+
import { esc, freshnessPageUrl, verifiedAge } from "./shared.mjs";
77

88
const RECENT_DAYS = 30;
99
const byName = (a, b) => a.entry.name.localeCompare(b.entry.name);
@@ -46,3 +46,108 @@ export function freshnessLineHtml(stats, stamp, href) {
4646
parts.push(`<a href="${esc(href)}">See every check, oldest first</a>.`);
4747
return parts.join(" ");
4848
}
49+
50+
const BUCKET_WORDS = { fresh: "fresh", aging: "aging", stale: "stale", unknown: "no check date" };
51+
52+
function recentHtml(recent, stamp) {
53+
if (!recent.length) return `<p>None in the ${RECENT_DAYS} days before ${esc(stamp)}.</p>`;
54+
const items = recent
55+
.map(
56+
(r) =>
57+
`<li><a href="../entry/${esc(r.entry.id)}/">${esc(r.entry.name)}</a> <span class="verified is-fresh">${esc(r.entry.verified)} (${dayWord(r.days)})</span></li>`
58+
)
59+
.join("\n ");
60+
return `<ul class="recent-list">
61+
${items}
62+
</ul>`;
63+
}
64+
65+
function rowHtml(r) {
66+
const date = r.entry.verified ? esc(r.entry.verified) : "none";
67+
const age = r.days === null ? "" : dayWord(r.days);
68+
return `<tr class="fresh-row" data-id="${esc(r.entry.id)}">
69+
<td data-label="Checked">${date}</td>
70+
<td data-label="Age">${age}</td>
71+
<td data-label="Status"><span class="verified is-${r.bucket}">${BUCKET_WORDS[r.bucket]}</span></td>
72+
<td data-label="Entry"><a href="../entry/${esc(r.entry.id)}/">${esc(r.entry.name)}</a></td>
73+
<td data-label="Licence">${esc(r.entry.license)}</td>
74+
</tr>`;
75+
}
76+
77+
export function freshnessPageHtml({ stats, site, stamp, total, hasCard }) {
78+
const url = freshnessPageUrl(site);
79+
const title = `Licence freshness | ${site.title}`;
80+
const desc = `When each licence in the catalog was last read at its source, oldest first. As of ${stamp}.`;
81+
const card = `${String(site.siteUrl).replace(/\/+$/, "")}/og-card.png`;
82+
const cardMeta = hasCard
83+
? `<meta property="og:image" content="${esc(card)}" />
84+
<meta name="twitter:card" content="summary_large_image" />
85+
<meta name="twitter:image" content="${esc(card)}" />`
86+
: `<meta name="twitter:card" content="summary" />`;
87+
return `<!DOCTYPE html>
88+
<html lang="en">
89+
<head>
90+
<meta charset="utf-8" />
91+
<meta name="viewport" content="width=device-width, initial-scale=1" />
92+
<title>${esc(title)}</title>
93+
<meta name="description" content="${esc(desc)}" />
94+
<link rel="canonical" href="${esc(url)}" />
95+
<meta property="og:type" content="website" />
96+
<meta property="og:site_name" content="${esc(site.title)}" />
97+
<meta property="og:title" content="${esc(title)}" />
98+
<meta property="og:description" content="${esc(desc)}" />
99+
<meta property="og:url" content="${esc(url)}" />
100+
${cardMeta}
101+
<meta name="twitter:title" content="${esc(title)}" />
102+
<meta name="twitter:description" content="${esc(desc)}" />
103+
<link rel="icon" href="../favicon.svg" type="image/svg+xml" />
104+
<link rel="preconnect" href="https://fonts.googleapis.com" />
105+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
106+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Public+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" />
107+
<link rel="stylesheet" href="../styles.css" />
108+
</head>
109+
<body>
110+
<a class="skip-link" href="#content">Skip to the list</a>
111+
<header class="topbar">
112+
<a class="brand" href="../">Free Game Dev Assets</a>
113+
<nav class="topnav" aria-label="Primary">
114+
<a href="../#catalog">Catalog</a>
115+
<a href="../#stacks">Stacks</a>
116+
<a href="../#starters">Starters</a>
117+
<a href="../#guides">Guides</a>
118+
<a href="${esc(site.repo)}" rel="noopener noreferrer">GitHub</a>
119+
</nav>
120+
</header>
121+
<main class="section entry-page freshness-page" id="content">
122+
<nav class="breadcrumb" aria-label="Breadcrumb">
123+
<a href="../#catalog">Catalog</a> <span aria-hidden="true">/</span>
124+
<span aria-current="page">Licence freshness</span>
125+
</nav>
126+
<h1>Licence freshness</h1>
127+
<p class="freshness-line">${freshnessLineHtml(stats, stamp, "#every-check")}</p>
128+
<div class="entry-lead"><p>An entry's verified date is when someone last read its licence at the source. Each entry's page quotes that licence with dates. Deprecated entries are not counted.</p></div>
129+
<section aria-labelledby="recent">
130+
<h2 id="recent">Most recently checked</h2>
131+
<p>Checked in the ${RECENT_DAYS} days before ${esc(stamp)}, newest first. The catalog keeps only the latest check date, so a first check and a recheck look the same.</p>
132+
${recentHtml(stats.recent, stamp)}
133+
</section>
134+
<section aria-labelledby="every-check">
135+
<h2 id="every-check">Every check, oldest first</h2>
136+
<table class="fresh-table">
137+
<thead>
138+
<tr><th scope="col">Checked</th><th scope="col">Age</th><th scope="col">Status</th><th scope="col">Entry</th><th scope="col">Licence</th></tr>
139+
</thead>
140+
<tbody>
141+
${stats.rows.map(rowHtml).join("\n ")}
142+
</tbody>
143+
</table>
144+
</section>
145+
</main>
146+
<footer class="footer">
147+
<p>Catalog metadata is CC0. Linked assets keep their own licenses: re-check the live source before shipping.</p>
148+
<p class="footer-stamp">Built ${esc(stamp)} from ${total} catalog entries.</p>
149+
</footer>
150+
</body>
151+
</html>
152+
`;
153+
}

‎site/lib/lib.test.mjs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,21 @@ const fEmpty = freshnessStats([], fNow);
401401
eq("empty catalog has no oldest", fEmpty.oldest, null);
402402
lacks("empty catalog line has no oldest clause", freshnessLineHtml(fEmpty, "2026-09-25", "f/"), "Oldest check");
403403

404+
/* freshness: page ---------------------------------------------------------- */
405+
import { freshnessPageHtml } from "./freshness.mjs";
406+
const fp = freshnessPageHtml({ stats: freshnessStats([...fEntries, fe("k", 'Q "<b>"', 5)], fNow), site, stamp: "2026-09-25", total: 319, hasCard: true });
407+
has("freshness title", fp, "<title>Licence freshness | Free Game Dev Assets</title>");
408+
has("freshness canonical", fp, '<link rel="canonical" href="https://x.test/s/freshness/" />');
409+
eq("freshness one h1", (fp.match(/<h1[\s>]/g) || []).length, 1);
410+
has("freshness links entries one level up", fp, 'href="../entry/i/"');
411+
has("freshness names escaped", fp, "Q &quot;&lt;b&gt;&quot;");
412+
has("freshness line links the table", fp, '<a href="#every-check">See every check, oldest first</a>');
413+
has("freshness table cells carry labels", fp, '<td data-label="Checked">');
414+
eq("freshness table rows oldest first", [...fp.matchAll(/<tr class="fresh-row" data-id="([^"]+)"/g)].map((m) => m[1]).slice(0, 3).join(","), "i,d,c");
415+
has("an undated row says so", fp, "no check date");
416+
has("recent section present", fp, 'id="recent"');
417+
lacks("deprecated left out of the page", fp, "Theta");
418+
404419
/* report (keep last) ------------------------------------------------------ */
405420
if (failures.length) {
406421
console.error(`lib.test failed (${failures.length}):`);

‎site/lib/shared.mjs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,8 @@ export function unquoteScalar(v) {
6363
export function stackPageUrl(site, id) {
6464
return `${String(site.siteUrl).replace(/\/+$/, "")}/stack/${id}/`;
6565
}
66+
67+
/** Absolute URL of the licence freshness page. */
68+
export function freshnessPageUrl(site) {
69+
return `${String(site.siteUrl).replace(/\/+$/, "")}/freshness/`;
70+
}

‎site/public/styles.css‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,3 +1240,104 @@ input:focus-visible {
12401240
margin: 0.5rem 0 0;
12411241
white-space: pre-wrap;
12421242
}
1243+
1244+
/* ------------------------------------------------------------------ *
1245+
* Licence freshness: the homepage line and /freshness/.
1246+
* ------------------------------------------------------------------ */
1247+
.freshness-line {
1248+
max-width: 42rem;
1249+
margin: 1rem 0 0;
1250+
color: var(--muted);
1251+
font-size: 0.95rem;
1252+
}
1253+
1254+
.freshness-line strong {
1255+
color: var(--ink);
1256+
}
1257+
1258+
.freshness-page section h2 {
1259+
margin: 1.75rem 0 0.5rem;
1260+
font-size: 1.15rem;
1261+
letter-spacing: -0.01em;
1262+
}
1263+
1264+
.recent-list {
1265+
margin: 0.5rem 0 0;
1266+
padding-left: 1.25rem;
1267+
}
1268+
1269+
.recent-list li {
1270+
margin: 0.3rem 0;
1271+
}
1272+
1273+
.fresh-table {
1274+
width: 100%;
1275+
border-collapse: collapse;
1276+
font-size: 0.9rem;
1277+
}
1278+
1279+
.fresh-table th,
1280+
.fresh-table td {
1281+
padding: 0.45rem 0.5rem;
1282+
border-bottom: 1px solid var(--line);
1283+
text-align: left;
1284+
vertical-align: top;
1285+
}
1286+
1287+
.fresh-table th {
1288+
font-family: var(--mono);
1289+
font-size: 0.75rem;
1290+
font-weight: 500;
1291+
letter-spacing: 0.04em;
1292+
text-transform: uppercase;
1293+
color: var(--muted);
1294+
}
1295+
1296+
.fresh-table td:nth-child(1),
1297+
.fresh-table td:nth-child(2) {
1298+
font-family: var(--mono);
1299+
white-space: nowrap;
1300+
}
1301+
1302+
@media (max-width: 640px) {
1303+
.fresh-table thead {
1304+
position: absolute;
1305+
width: 1px;
1306+
height: 1px;
1307+
overflow: hidden;
1308+
clip: rect(0 0 0 0);
1309+
}
1310+
1311+
.fresh-table,
1312+
.fresh-table tbody,
1313+
.fresh-table tr,
1314+
.fresh-table td {
1315+
display: block;
1316+
width: 100%;
1317+
}
1318+
1319+
.fresh-table tr {
1320+
padding: 0.5rem 0;
1321+
border-bottom: 1px solid var(--line);
1322+
}
1323+
1324+
.fresh-table td {
1325+
display: flex;
1326+
gap: 0.75rem;
1327+
padding: 0.15rem 0;
1328+
border: 0;
1329+
}
1330+
1331+
.fresh-table td::before {
1332+
content: attr(data-label);
1333+
flex: 0 0 5.5rem;
1334+
font-family: var(--mono);
1335+
font-size: 0.75rem;
1336+
color: var(--muted);
1337+
}
1338+
1339+
.fresh-table td:nth-child(1),
1340+
.fresh-table td:nth-child(2) {
1341+
white-space: normal;
1342+
}
1343+
}

0 commit comments

Comments
 (0)