Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ per entry, columns in this order: id, name, url, category, license, license_spdx
commercial, attribution_required, formats, tags, added, verified, status, page,
subcategories, publisher, attribution_string; new columns are appended, never inserted. List
fields are joined with `; `, and a cell that starts with `=`, `+`, `-`, `@`, a tab or a
carriage return gets a leading `'` so a spreadsheet does not run it as a formula) and two
Atom feeds:
carriage return gets a leading `'` so a spreadsheet does not run it as a formula),
[`opensearch.xml`](https://tmhsdigital.github.io/Free-Game-Dev-Assets/opensearch.xml)
(an OpenSearch 1.1 description document so browsers can add the catalog as a search engine)
and two Atom feeds:
[`feed.xml`](https://tmhsdigital.github.io/Free-Game-Dev-Assets/feed.xml), the 40 newest
entries by `added` (the day each joined the catalog), and
[`reverified.xml`](https://tmhsdigital.github.io/Free-Game-Dev-Assets/reverified.xml), the
Expand Down Expand Up @@ -141,7 +143,7 @@ not counted. `pages.yml` also runs every Monday at 06:17 UTC so the ages keep mo
when nothing is pushed; GitHub pauses scheduled workflows in a public repository after
60 days without activity, so a long quiet spell needs a manual run from the Actions tab.

## Sitemap
## Sitemap and OpenSearch

`build.mjs` writes `sitemap.xml` (home, freshness, category pages, stacks and every non-deprecated entry)
and links it from the homepage with `<link rel="sitemap">`. The `Sitemap:` line in
Expand All @@ -150,6 +152,9 @@ under `/Free-Game-Dev-Assets/` it is never seen. Submit
`https://tmhsdigital.github.io/Free-Game-Dev-Assets/sitemap.xml` once in Google Search
Console and Bing Webmaster Tools; that is the step that gets it read.

`opensearch.xml` is published and linked from the homepage with `<link rel="search">`
so browsers can discover and add the catalog as a search engine using `{searchTerms}` on `?q=`.

## Link check

`.github/workflows/links.yml` runs `site/check-links.mjs` every Monday at 07:41 UTC (and
Expand Down
5 changes: 3 additions & 2 deletions site/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { parseFrontmatter, summaryFromBody } from "./lib/frontmatter.mjs";
import { listEntryFiles, idMatchesFile } from "./lib/entry-files.mjs";
import { entryPageHtml, scriptJson } from "./lib/entry-page.mjs";
import { LinkError, makeLinkResolver, caseMismatch } from "./lib/links.mjs";
import { atomFeed, catalogCsv, commitDatesFromLog, DATED_PATHS, homeJsonLd, sitemapXml } from "./lib/exports.mjs";
import { atomFeed, catalogCsv, commitDatesFromLog, DATED_PATHS, homeJsonLd, openSearchXml, sitemapXml } from "./lib/exports.mjs";
import { schemaErrors } from "./lib/schema-check.mjs";
import { llmsFullTxt, llmsTxt } from "./lib/llms.mjs";
import { deprecationReason, MarkdownError, renderBlocks, renderInline, splitEntryBody } from "./lib/markdown.mjs";
Expand Down Expand Up @@ -704,6 +704,7 @@ function main() {
const modified = lastCommitDates();
writeOut("sitemap.xml", sitemapXml({ site: config.site, entries: visible, stacks, stamp, modified, categories: categoriesInSitemap, guides: config.guides }));
writeOut("robots.txt", robotsTxt(config.site));
writeOut("opensearch.xml", openSearchXml({ site: config.site }));
writeOut("404.html", notFoundHtml(config.site));
writeOut(path.join("freshness", "index.html"), freshnessPageHtml({ stats: fresh, site: config.site, stamp, total: stats.total, hasCard }));

Expand Down Expand Up @@ -748,7 +749,7 @@ function main() {
`Built ${entries.length} entries → site/dist (${stats.active} active, ${stats.commercialOk} commercial-ok, ${stats.commercialVaries} per-file)`
);
console.log(
`Prerendered ${visible.length} entry rows into index.html; wrote ${pages.written.length} entry pages, ${stackPages.written.length} stack pages, ${categoryPages.written.length} category pages, ${guidePages.written.length} guide pages, sitemap.xml, robots.txt, 404.html, freshness/index.html`
`Prerendered ${visible.length} entry rows into index.html; wrote ${pages.written.length} entry pages, ${stackPages.written.length} stack pages, ${categoryPages.written.length} category pages, ${guidePages.written.length} guide pages, sitemap.xml, robots.txt, 404.html, freshness/index.html, opensearch.xml`
);
console.log(`${writes} of ${produced.size} output files changed${clean ? " (clean build)" : ""}; ${((Date.now() - started) / 1000).toFixed(1)}s`);
}
Expand Down
26 changes: 24 additions & 2 deletions site/lib/exports.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Flat exports of the catalog: catalog.csv for spreadsheets, feed.xml (Atom)
* for readers and sitemap.xml for crawlers. data.json stays the full
* machine-readable catalog.
* for readers, sitemap.xml for crawlers and opensearch.xml for browsers.
* data.json stays the full machine-readable catalog.
*/
import { esc } from "./shared.mjs";

Expand Down Expand Up @@ -52,6 +52,28 @@ export function sitemapXml({ site, entries, stacks = [], stamp, modified = new M
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join("\n")}\n</urlset>\n`;
}

/**
* opensearch.xml. An OpenSearch 1.1 description document so browsers can add
* the catalog as a search engine and search it directly from the address bar.
*/
export function openSearchXml(siteOrOpts) {
const site = siteOrOpts?.site || siteOrOpts;
const base = String(site?.siteUrl || "").replace(/\/+$/, "");
return [
'<?xml version="1.0" encoding="UTF-8"?>',
'<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">',
` <ShortName>${esc(site?.title)}</ShortName>`,
` <Description>${esc(site?.tagline)}</Description>`,
' <InputEncoding>UTF-8</InputEncoding>',
` <Image height="16" width="16" type="image/svg+xml">${esc(base)}/favicon.svg</Image>`,
` <Url type="text/html" template="${esc(base)}/?q={searchTerms}" />`,
'</OpenSearchDescription>',
'',
].join('\n');
}

export const opensearchXml = openSearchXml;

const CSV_COLUMNS = [
"id",
"name",
Expand Down
43 changes: 43 additions & 0 deletions site/lib/lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,49 @@ eq("search: q from the URL is capped", readState(`?q=${"a".repeat(5000)}`, { q:
has("safe-fetch: DNS failure keeps its code", await errOf(safeFetch("https://missing.example/", {}, opts)), "ENOTFOUND");
}

/* opensearch.xml (#241) ---------------------------------------------------- */
import { openSearchXml } from "./exports.mjs";
{
const osSite = {
title: "Free Game Dev Assets",
tagline: "Free assets and tools for commercial games, with every licence read at its source.",
siteUrl: "https://tmhsdigital.github.io/Free-Game-Dev-Assets/",
};
const xml = openSearchXml({ site: osSite });
has("opensearch: xml header", xml, '<?xml version="1.0" encoding="UTF-8"?>');
has("opensearch: OpenSearchDescription root with namespace", xml, '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">');
has("opensearch: ShortName matches site title", xml, "<ShortName>Free Game Dev Assets</ShortName>");
has("opensearch: Description matches site tagline", xml, "<Description>Free assets and tools for commercial games, with every licence read at its source.</Description>");
has("opensearch: InputEncoding is UTF-8", xml, "<InputEncoding>UTF-8</InputEncoding>");
has("opensearch: Image points to favicon.svg", xml, '<Image height="16" width="16" type="image/svg+xml">https://tmhsdigital.github.io/Free-Game-Dev-Assets/favicon.svg</Image>');
has("opensearch: Url template queries ?q={searchTerms}", xml, '<Url type="text/html" template="https://tmhsdigital.github.io/Free-Game-Dev-Assets/?q={searchTerms}" />');

// Trailing slash handling and XML escaping
const xmlEscaped = openSearchXml({
site: {
title: "Assets & Tools <Free>",
tagline: 'Quotes "and" & signs',
siteUrl: "https://example.com/catalog",
},
});
has("opensearch: title is XML escaped", xmlEscaped, "<ShortName>Assets &amp; Tools &lt;Free&gt;</ShortName>");
has("opensearch: tagline is XML escaped", xmlEscaped, "<Description>Quotes &quot;and&quot; &amp; signs</Description>");
has("opensearch: Url template handles siteUrl without trailing slash", xmlEscaped, '<Url type="text/html" template="https://example.com/catalog/?q={searchTerms}" />');
has("opensearch: Image handles siteUrl without trailing slash", xmlEscaped, '<Image height="16" width="16" type="image/svg+xml">https://example.com/catalog/favicon.svg</Image>');

// Direct site argument overload
const xmlDirect = openSearchXml(osSite);
eq("opensearch: accepts direct site object", xmlDirect, xml);

// Homepage links opensearch.xml
const indexHtml = nodeFs.readFileSync(new URL("../public/index.html", import.meta.url), "utf8");
has(
"homepage links opensearch.xml",
indexHtml,
'<link rel="search" type="application/opensearchdescription+xml" title="Free Game Dev Assets" href="opensearch.xml" />'
);
}

/* report (keep last) ------------------------------------------------------ */
if (failures.length) {
console.error(`lib.test failed (${failures.length}):`);
Expand Down
1 change: 1 addition & 0 deletions site/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
name="description"
content="Curated free game assets and tools, with every licence read at its source."
/>
<link rel="search" type="application/opensearchdescription+xml" title="Free Game Dev Assets" href="opensearch.xml" />
<!--HEAD_META-->
</head>
<body>
Expand Down