Files
lucide/scripts/updateSponsors.mts
14cab02398 feat(site): Add sponsor page (#5132)
* feat(docs): add sponsor page

Add a /sponsor page to lucide.dev with sponsor tiers, an amount picker,
a sponsor wall and a FAQ.

- Fetch active Open Collective sponsors at build time into
  sponsorsData.json, falling back to an empty list when the API fails
- Group sponsors by monthly amount so the wall keeps working while the
  Open Collective tiers are renamed
- Add reusable SponsorLogoGrid and SponsorAvatars components
- Add general Faq/FaqItem components fed by `faq` frontmatter, which
  also emits FAQPage structured data
- Link the page from the nav, footer and the home page sponsor card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(docs): add navbar sponsor button, more sponsor stats and tier links

- Replace the Sponsor nav text link with a dedicated heart button in the
  navbar, shown icon-only at tablet widths where the menu is crowded
- Show weekly npm downloads across all Lucide packages and the all-time
  Open Collective sponsor count in the sponsor page hero
- Add tierLinks.ts for direct Open Collective and GitHub Sponsors tier
  checkout links, falling back to the general pages while empty

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(docs): update team member titles and add funding manifest

* update tier links

* Adjust hero

* Add motion counter to hero

* Add custom option

* Add scroll padding

* Fix reduce motion sponsor page

* Refactor sponsor tier links into tier data

Store only the Open Collective tier slug and GitHub Sponsors tier id on
each tier and derive the checkout URLs from them, so amounts are no
longer duplicated in the links. Removes tierLinks.ts and the circular
import between it and tiers.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9u8FefqMHXb8HoeQ8LhcU

* Set transition duration to zero

* Add social links

* Fix stikcybar

* Format code

* Refactor SponsorAmountPicker and SponsorHeroAnimation

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix sponsor animation

* Adjust tiers

* Update readmes

* Update sponsors

* Update sponsors

* Update sponsors

* Update logo gris size

* Add breaking space

* Update logos

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-11-02 12:21:25 +01:00

155 lines
6.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TIERS, type TierId } from '../docs/.vitepress/theme/components/sponsor/tiers.ts';
import {
type Account,
fetchCollective,
getCurrentSponsors,
getLocalLogos,
getOpenCollectiveLogo,
getWebsite,
} from '../docs/scripts/helpers/openCollective.mts';
const rootPath = join(import.meta.dirname, '..');
const mainReadmePath = join(rootPath, 'README.md');
const tiersStartMarker = '[//]: <> (Sponsor tiers start)';
const tiersEndMarker = '[//]: <> (Sponsor tiers end)';
const mainReadmeOnlyStartMarker = '[//]: <> (Main README only start)';
const mainReadmeOnlyEndMarker = '[//]: <> (Main README only end)';
/**
* How each sponsor tier shows up in the READMEs, following the tier perks in tiers.ts.
* Tiers that are not in the npm package READMEs are wrapped in the "Main README only" markers.
* Only tiers with `customLogo` use the logos in docs/public/sponsors, the others use their avatar.
*/
const README_TIERS: Record<
TierId,
{ title: string; logoHeight: number; inPackages: boolean; customLogo: boolean }
> = {
platinum: { title: 'Platinum 💎', logoHeight: 96, inPackages: true, customLogo: true },
gold: { title: 'Gold 🥇', logoHeight: 64, inPackages: true, customLogo: true },
bronze: { title: 'Bronze 🥉', logoHeight: 48, inPackages: true, customLogo: true },
backer: { title: 'Backers 🍺', logoHeight: 32, inPackages: false, customLogo: false },
supporter: { title: 'Supporters ☕️', logoHeight: 32, inPackages: false, customLogo: false },
};
const escapeHtml = (value: string) =>
value
.replaceAll('&', '&amp;')
.replaceAll('"', '&quot;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
/** Uses a `<picture>` to switch to the dark logo in dark mode, when there is one. */
function renderSponsor(account: Account, height: number, customLogo: boolean) {
const logos: { light?: string; dark?: string } = customLogo ? getLocalLogos(account.slug) : {};
const src = logos.light ? `docs/public/sponsors/${logos.light}` : getOpenCollectiveLogo(account);
const alt = account.description ? `${account.name} – ${account.description}` : account.name;
const image = `<img src="${escapeHtml(src)}" height="${height}" alt="${escapeHtml(alt)}" />`;
const logo = logos.dark
? `<picture><source media="(prefers-color-scheme: dark)" srcset="docs/public/sponsors/${escapeHtml(logos.dark)}" />${image}</picture>`
: image;
return `<a href="${escapeHtml(getWebsite(account))}">${logo}</a>`;
}
function renderTiers({ orders }: Awaited<ReturnType<typeof fetchCollective>>) {
const sponsorsByTier = Map.groupBy(getCurrentSponsors(orders.nodes), ({ tier }) => tier);
// Highest tier first, so Platinum sponsors are at the top.
const renderTierGroup = (inPackages: boolean) =>
[...TIERS]
.reverse()
.filter(({ id }) => README_TIERS[id].inPackages === inPackages)
.flatMap(({ id }) => {
const sponsors = sponsorsByTier.get(id) ?? [];
if (!sponsors.length) return [];
const { title, logoHeight, customLogo } = README_TIERS[id];
const sponsorLinks = sponsors
.map(({ account }) => account)
.sort((a, b) => a.name.localeCompare(b.name))
.map((account) => renderSponsor(account, logoHeight, customLogo))
// READMEs strip `style`, so non-breaking spaces are used to space out the logos.
.join('\n&nbsp;&nbsp;\n');
return `### ${title}\n\n${sponsorLinks}`;
})
.join('\n\n');
const packageTiers = renderTierGroup(true);
const mainReadmeOnlyTiers = renderTierGroup(false);
return [
packageTiers,
mainReadmeOnlyTiers &&
[mainReadmeOnlyStartMarker, mainReadmeOnlyTiers, mainReadmeOnlyEndMarker].join('\n\n'),
]
.filter(Boolean)
.join('\n\n');
}
async function updateReadmeTiers() {
const mainReadme = readFileSync(mainReadmePath, 'utf8');
const startIndex = mainReadme.indexOf(tiersStartMarker);
const endIndex = mainReadme.indexOf(tiersEndMarker);
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
throw new Error(`Could not find "${tiersStartMarker}" and "${tiersEndMarker}" in README.md`);
}
const tiersMarkup = renderTiers(await fetchCollective());
writeFileSync(
mainReadmePath,
[
mainReadme.slice(0, startIndex + tiersStartMarker.length),
tiersMarkup,
mainReadme.slice(endIndex),
]
.filter(Boolean)
.join('\n\n'),
);
console.log(`Updated sponsor tiers in ${mainReadmePath}`);
}
function updatePackageSponsors() {
const packagesPath = join(rootPath, 'packages');
const mainReadme = readFileSync(mainReadmePath, 'utf8');
const sponsorsRegex = /(\[\/\/]: <> \(Sponsors\).+)$/s;
const mainReadmeOnlyRegex =
/\[\/\/]: <> \(Main README only start\).*?\[\/\/]: <> \(Main README only end\)\s*/gs;
const sponsorsBlock = sponsorsRegex.exec(mainReadme);
if (sponsorsBlock?.[1]) {
const packageSponsorsBlock = sponsorsBlock[1]
.replace(mainReadmeOnlyRegex, '')
.replaceAll('="docs/public/', '="https://lucide.dev/');
readdirSync(packagesPath).forEach((packagePath) => {
if (['lucide-figma', 'shared'].includes(packagePath) || packagePath.startsWith('.')) {
return;
}
const readmePath = join(packagesPath, packagePath, 'README.md');
if (existsSync(readmePath)) {
const readmeContent = readFileSync(readmePath, 'utf8');
if (sponsorsRegex.test(readmeContent)) {
writeFileSync(readmePath, readmeContent.replace(sponsorsRegex, packageSponsorsBlock));
console.log(`Upgraded Sponsors block in ${readmePath}`);
} else {
console.error(`Could not find Sponsors block in ${readmePath}`);
}
} else {
console.warn(`No readme found at ${readmePath}`);
}
});
} else {
console.error('Could not identify Sponsors block');
}
}
if (!process.argv.includes('--skip-fetch')) {
await updateReadmeTiers();
}
updatePackageSponsors();