Skip to content

Commit 4ef2051

Browse files
authored
chore: sync sponsors with new upstream schema (#71)
Ports the sponsor-sync changes from middleapi/orpc#1897 to this repo's `scripts/sync-sponsors.ts` and regenerates the sponsors section in every README. Slot sponsors now appear as featured full-width cards above the tier tables, and every link carries the tracking and `rel` tokens the upstream data provides. ## Changes - Sponsor links are no longer rewritten with `ref=orpc` — they arrive pre-tracked from upstream, and each link's `rel` is `noopener` plus the sponsor's own tokens instead of a hardcoded `sponsored noopener`. - Sponsors that bought an ad slot render as full-width cards (floated logo, name over tagline) and are excluded from the tier tables, so nobody appears twice. - Tier headings use the new plural tier titles; image sizes rank against all active tiers including the featured ones, so the tables keep their previous 167/139/119 widths. - The past-sponsors line links to the full sponsor wall SVG. - Section replacement trims to a single trailing newline, so re-syncs no longer fight eslint's markdown fixer over the last line of a README. The PR's `apps/content` parts (`slots.ts` generation, ad-card component) have no counterpart here and were skipped. ## Testing - `pnpm run sponsors:sync` passes with eslint at `--max-warnings=0`, and a second run leaves every README byte-identical. - `scripts/sync-sponsors.ts` passes eslint and `tsc --noEmit`.
1 parent 2c489f1 commit 4ef2051

10 files changed

Lines changed: 408 additions & 426 deletions

File tree

‎packages/aws-lambda/README.md‎

Lines changed: 38 additions & 45 deletions
Large diffs are not rendered by default.

‎packages/core/README.md‎

Lines changed: 38 additions & 45 deletions
Large diffs are not rendered by default.

‎packages/fastify/README.md‎

Lines changed: 38 additions & 45 deletions
Large diffs are not rendered by default.

‎packages/fetch/README.md‎

Lines changed: 38 additions & 45 deletions
Large diffs are not rendered by default.

‎packages/node/README.md‎

Lines changed: 38 additions & 45 deletions
Large diffs are not rendered by default.

‎packages/peer/README.md‎

Lines changed: 38 additions & 45 deletions
Large diffs are not rendered by default.

‎packages/shared/README.md‎

Lines changed: 38 additions & 45 deletions
Large diffs are not rendered by default.

‎scripts/sync-sponsors.ts‎

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,22 @@ interface Sponsor {
99
amount: number
1010
createdAt: string
1111
tierTitle: string
12+
tierTitlePlural: string
1213
tierLevel: number
14+
/** Extra rel tokens for the sponsor's link (e.g. `sponsored`); may be empty. */
15+
rel: string
1316
link: string
14-
org: boolean
15-
sidebarSize?: string
16-
sidebarLogo?: string
17+
/** Tagline, present on sponsors that bought an ad slot. */
18+
description?: string
19+
/** Brand tint behind the ad card, both themes required together. */
20+
background?: { light: string, dark: string }
21+
/** 1-based ad-grid position the sponsor bought. */
22+
slot?: number
1723
[key: string]: unknown
1824
}
1925

2026
const SPONSORS_SOURCE_URL = 'https://raw.githubusercontent.com/middleapi/static/refs/heads/main/sponsors.json'
27+
const PAST_SPONSORS_URL = 'https://htmlpreview.github.io/?https://github.com/middleapi/static/blob/main/sponsors.svg'
2128
const ROOT_DIR = process.cwd()
2229
const README_FILE_NAME = 'README.md'
2330

@@ -47,17 +54,9 @@ async function findReadmes(dir: string): Promise<string[]> {
4754
return result.concat(...subResults)
4855
}
4956

50-
function withTracking(url: string): string {
51-
try {
52-
const tracked = new URL(url)
53-
54-
tracked.searchParams.set('ref', 'orpc')
55-
56-
return tracked.toString()
57-
}
58-
catch {
59-
return url
60-
}
57+
/** `noopener` always, plus whatever extra tokens the data carries. */
58+
function relAttribute(sponsor: Sponsor): string {
59+
return ['noopener', sponsor.rel].filter(Boolean).join(' ')
6160
}
6261

6362
function escapeHtml(value: string): string {
@@ -77,13 +76,48 @@ function getTierImageSizeAndColumns(tierLevel: number, tierLevels: number[]): [c
7776
return [column, Math.floor(838 / column)]
7877
}
7978

79+
/**
80+
* The slot sponsors' cards, mirroring the docs ad cards — logo on the left,
81+
* name over tagline on the right — but without the brand backgrounds (GitHub
82+
* strips inline styles anyway). One card per row. The oversized cell width is
83+
* a preferred width, not a minimum: GitHub caps tables at the container, so
84+
* the card spans the full row on any screen and the text wraps on phones
85+
* instead of scrolling. One cell per card, the logo floated with the
86+
* deprecated-but-sanitizer-safe `align`/`hspace` attributes, so no table
87+
* border cuts through the card.
88+
*/
89+
function buildSlotCards(slotSponsors: Sponsor[]): string[] {
90+
const lines = ['<table>']
91+
92+
for (const sponsor of slotSponsors) {
93+
const name = escapeHtml(sponsor.name ?? sponsor.login)
94+
const description = escapeHtml(sponsor.description ?? '')
95+
96+
lines.push(' <tr>')
97+
lines.push(` <td width="2000"><a href="${escapeHtml(sponsor.link)}" target="_blank" rel="${relAttribute(sponsor)}" title="${description}"><img src="${escapeHtml(sponsor.avatar)}" width="64" align="left" hspace="12" alt="${name}"/><b>${name}</b></a><br /><sub>${description}</sub></td>`)
98+
lines.push(' </tr>')
99+
}
100+
101+
lines.push('</table>')
102+
lines.push('')
103+
104+
return lines
105+
}
106+
80107
function buildSponsorsSection(sponsors: Sponsor[]): string {
81108
const activeSponsors = sponsors.filter(sponsor => sponsor.tierLevel > 0 && sponsor.amount > 0)
82109
const pastSponsors = sponsors.filter(sponsor => sponsor.tierLevel <= 0 || sponsor.amount <= 0)
83110

111+
// Slot sponsors are featured as cards up top; the tier tables below carry
112+
// everyone else so nobody appears twice.
113+
const slotSponsors = activeSponsors
114+
.filter(sponsor => sponsor.slot !== undefined)
115+
.sort((a, b) => a.slot! - b.slot!)
116+
const tieredSponsors = activeSponsors.filter(sponsor => sponsor.slot === undefined)
117+
84118
const groupedSponsors = new Map<number, Sponsor[]>()
85119

86-
for (const sponsor of activeSponsors) {
120+
for (const sponsor of tieredSponsors) {
87121
const group = groupedSponsors.get(sponsor.tierLevel)
88122

89123
if (group) {
@@ -101,6 +135,13 @@ function buildSponsorsSection(sponsors: Sponsor[]): string {
101135
'',
102136
]
103137

138+
if (slotSponsors.length > 0) {
139+
lines.push(...buildSlotCards(slotSponsors))
140+
}
141+
142+
// Sizes rank against every active tier, slot sponsors' tiers included, so
143+
// featuring the top tiers as cards does not inflate the tables below them.
144+
const sizeTierLevels = [...new Set(activeSponsors.map(sponsor => sponsor.tierLevel))].sort((a, b) => b - a)
104145
const tierLevels = [...groupedSponsors.keys()].sort((a, b) => b - a)
105146

106147
for (const tierLevel of tierLevels) {
@@ -110,8 +151,8 @@ function buildSponsorsSection(sponsors: Sponsor[]): string {
110151
continue
111152
}
112153

113-
const tierTitle = tierSponsors[0]?.tierTitle ?? `Tier ${tierLevel}`
114-
const [columns, imageSize] = getTierImageSizeAndColumns(tierLevel, tierLevels)
154+
const tierTitle = tierSponsors[0]?.tierTitlePlural ?? `Tier ${tierLevel}`
155+
const [columns, imageSize] = getTierImageSizeAndColumns(tierLevel, sizeTierLevels)
115156

116157
lines.push(`### ${tierTitle}`)
117158
lines.push('')
@@ -123,7 +164,7 @@ function buildSponsorsSection(sponsors: Sponsor[]): string {
123164
const displayName = sponsor.name ?? sponsor.login
124165
const escapedName = escapeHtml(displayName)
125166

126-
lines.push(` <td align="center"><a href="${escapeHtml(href)}" target="_blank" rel="sponsored noopener" title="${escapedName}"><img src="${escapeHtml(sponsor.avatar)}" width="${imageSize}" alt="${escapedName}"/><br />${escapedName}</a></td>`)
167+
lines.push(` <td align="center"><a href="${escapeHtml(href)}" target="_blank" rel="${relAttribute(sponsor)}" title="${escapedName}"><img src="${escapeHtml(sponsor.avatar)}" width="${imageSize}" alt="${escapedName}"/><br />${escapedName}</a></td>`)
127168

128169
const isRowEnd = (index + 1) % columns === 0
129170
const isLast = index === tierSponsors.length - 1
@@ -142,7 +183,7 @@ function buildSponsorsSection(sponsors: Sponsor[]): string {
142183
if (pastSponsors.length > 0) {
143184
const noun = pastSponsors.length === 1 ? 'past sponsor' : 'past sponsors'
144185

145-
lines.push(`With thanks to ${pastSponsors.length} ${noun} who helped get us here.`)
186+
lines.push(`With thanks to [${pastSponsors.length} ${noun}](${PAST_SPONSORS_URL}) who helped get us here.`)
146187
lines.push('')
147188
}
148189

@@ -160,7 +201,10 @@ function replaceSponsorsSection(content: string, replacement: string): string {
160201
const nextHeadingIndex = content.indexOf('\n## ', startIndex + heading.length)
161202
const endIndex = nextHeadingIndex === -1 ? content.length : nextHeadingIndex + 1
162203

163-
return `${content.slice(0, startIndex)}${replacement}${content.slice(endIndex)}`
204+
// Trimmed to a single trailing newline, or a section replaced at the end of
205+
// the file leaves a blank last line that eslint's markdown fixer removes —
206+
// and the next sync would put back, forever.
207+
return `${`${content.slice(0, startIndex)}${replacement}${content.slice(endIndex)}`.trimEnd()}\n`
164208
}
165209

166210
async function main(): Promise<void> {
@@ -170,7 +214,8 @@ async function main(): Promise<void> {
170214
throw new Error(`Failed to fetch sponsors data: ${response.status} ${response.statusText}`)
171215
}
172216

173-
const sponsors = (await response.json() as Sponsor[]).map(sponsor => ({ ...sponsor, link: withTracking(sponsor.link) }))
217+
// Links arrive with their tracking params already baked in upstream.
218+
const sponsors = await response.json() as Sponsor[]
174219
const readmeFiles = await findReadmes(ROOT_DIR)
175220
const replacement = buildSponsorsSection(sponsors)
176221

0 commit comments

Comments
 (0)