diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml
index 433caed..bd29fef 100644
--- a/.github/workflows/sponsors.yml
+++ b/.github/workflows/sponsors.yml
@@ -1,8 +1,9 @@
name: Sponsors
-# Keeps docs/sponsors.json current so the website's sponsor wall features
-# sponsors automatically — new sponsors appear within a day of sponsoring,
-# lapsed ones roll off, no manual list edits.
+# Keeps docs/sponsors.json and the README sponsor block current so the website's
+# sponsor wall and the repo page feature sponsors automatically — new sponsors
+# appear within a few hours of sponsoring, lapsed ones roll off, no manual list
+# edits.
#
# The Sponsors GraphQL API rejects anonymous reads, so this runs here with a
# token. If the default Actions token can't read the org's sponsorships, add
@@ -11,12 +12,13 @@ name: Sponsors
on:
schedule:
- - cron: '43 4 * * *' # daily, 04:43 UTC
+ - cron: '43 */3 * * *' # every 3 hours — a new sponsor shouldn't wait a day
workflow_dispatch: # refresh on demand
push:
branches: [main]
paths:
- 'scripts/sponsors.mjs'
+ - 'scripts/lib/sponsorsReadme.mjs'
- '.github/workflows/sponsors.yml'
permissions:
@@ -44,7 +46,7 @@ jobs:
run: |
# Stage first: on the very first run the file is untracked, and
# `git diff` alone would report no change and skip the seed commit.
- git add -A docs/sponsors.json
+ git add -A docs/sponsors.json README.md
if git diff --cached --quiet; then
echo "No change."
exit 0
diff --git a/README.md b/README.md
index dfa122b..115970e 100644
--- a/README.md
+++ b/README.md
@@ -237,7 +237,13 @@ Quickdraw is MIT-licensed with no paid tier — [sponsors](https://github.com/sp
are what keep it that way. Sponsors get their logo on
[the website](https://tryquickdraw.com/sponsors/) and here:
-No sponsors yet — your logo could be the first.
+
+
+
## Star history
diff --git a/scripts/lib/sponsorsReadme.mjs b/scripts/lib/sponsorsReadme.mjs
new file mode 100644
index 0000000..19811e9
--- /dev/null
+++ b/scripts/lib/sponsorsReadme.mjs
@@ -0,0 +1,39 @@
+// Renders the sponsor block that lives between the markers in README.md, so
+// the repo page shows the same wall as the website. Pure string-in/string-out
+// — scripts/sponsors.mjs does the file I/O.
+
+export const START = ''
+export const END = ''
+
+const SPONSORS_PAGE = 'https://tryquickdraw.com/sponsors/'
+const EMPTY = `No sponsors yet — your logo could be the first.`
+
+const esc = (s) =>
+ String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"')
+
+// GitHub strips styles from README HTML, so layout is a plain table (the
+// all-contributors trick): avatar on top, name under it, PER_ROW to a row.
+const PER_ROW = 6
+
+export function renderSponsorsBlock(sponsors) {
+ if (!sponsors.length) return EMPTY
+ const cells = sponsors
+ .slice()
+ .sort((a, b) => (b.monthly ?? 0) - (a.monthly ?? 0) || (a.since ?? '').localeCompare(b.since ?? ''))
+ .map((s) => {
+ const avatar = s.avatar + (s.avatar.includes('?') ? '&' : '?') + 's=128'
+ return `}) ${esc(s.name)} | `
+ })
+ const rows = []
+ for (let i = 0; i < cells.length; i += PER_ROW) rows.push(`\n${cells.slice(i, i + PER_ROW).join('\n')}\n
`)
+ return ``
+}
+
+// Returns the README with the block between the markers replaced, or null
+// when the markers are missing — the caller decides how loud to be about it.
+export function replaceSponsorsBlock(readme, sponsors) {
+ const a = readme.indexOf(START)
+ const b = readme.indexOf(END)
+ if (a === -1 || b === -1 || b < a) return null
+ return `${readme.slice(0, a + START.length)}\n${renderSponsorsBlock(sponsors)}\n${readme.slice(b)}`
+}
diff --git a/scripts/sponsors.mjs b/scripts/sponsors.mjs
index 05a6db6..2415ea3 100644
--- a/scripts/sponsors.mjs
+++ b/scripts/sponsors.mjs
@@ -3,6 +3,8 @@
// website's sponsor wall updates itself — nobody edits a list by hand when a
// sponsorship starts or lapses. Same shape as star-history: CI runs this on a
// schedule and commits the snapshot; the static build just reads the file.
+// The same run rewrites the sponsor block between the markers in README.md,
+// so the repo page never lags the website.
//
// Reading sponsorships needs an authenticated token. The Actions token can
// usually read an org's public sponsors; if GitHub rejects it, add a classic
@@ -11,12 +13,14 @@
//
// GITHUB_TOKEN= node scripts/sponsors.mjs
-import { writeFileSync, mkdirSync } from 'node:fs'
+import { writeFileSync, mkdirSync, readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
+import { replaceSponsorsBlock } from './lib/sponsorsReadme.mjs'
const LOGIN = process.env.SPONSORS_LOGIN ?? 'quickdrawjs'
-const OUT = join(dirname(fileURLToPath(import.meta.url)), '..', 'docs')
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
+const OUT = join(ROOT, 'docs')
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN
if (!token) {
@@ -88,9 +92,32 @@ const sponsors = (conn.nodes ?? [])
since: n.createdAt?.slice(0, 10) ?? null,
}))
-mkdirSync(OUT, { recursive: true })
-writeFileSync(
- join(OUT, 'sponsors.json'),
- JSON.stringify({ generatedAt: new Date().toISOString(), login: LOGIN, sponsors }, null, 2) + '\n',
-)
-console.log(`sponsors: wrote ${sponsors.length} sponsor(s) to docs/sponsors.json`)
+// Only rewrite the snapshot when the wall actually changed — a fresh
+// generatedAt alone would mean a commit (and a site deploy) on every run.
+const snapshotPath = join(OUT, 'sponsors.json')
+let previous = null
+try {
+ previous = JSON.parse(readFileSync(snapshotPath, 'utf8')).sponsors
+} catch {
+ // first run — no snapshot yet
+}
+if (JSON.stringify(previous) === JSON.stringify(sponsors)) {
+ console.log(`sponsors: ${sponsors.length} sponsor(s), unchanged`)
+} else {
+ mkdirSync(OUT, { recursive: true })
+ writeFileSync(
+ snapshotPath,
+ JSON.stringify({ generatedAt: new Date().toISOString(), login: LOGIN, sponsors }, null, 2) + '\n',
+ )
+ console.log(`sponsors: wrote ${sponsors.length} sponsor(s) to docs/sponsors.json`)
+}
+
+const readmePath = join(ROOT, 'README.md')
+const readme = readFileSync(readmePath, 'utf8')
+const next = replaceSponsorsBlock(readme, sponsors)
+if (next === null) {
+ console.warn('sponsors: README.md has no sponsors markers — left it alone')
+} else if (next !== readme) {
+ writeFileSync(readmePath, next)
+ console.log('sponsors: updated the README sponsor block')
+}