This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-website.git
The following commit(s) were added to refs/heads/master by this push:
new b785b830 feat: replace github star badges with SVG images (#464)
b785b830 is described below
commit b785b8303214b9e1c239a31b26b7bc7c22cdaa44
Author: cooronx <[email protected]>
AuthorDate: Thu Apr 16 21:58:30 2026 +0800
feat: replace github star badges with SVG images (#464)
---
.github/workflows/master.yml | 3 +
.github/workflows/pr_ci.yml | 3 +
docusaurus.config.js | 68 +------------
package.json | 1 +
scripts/generate-footer-stars.js | 90 +++++++++++++++++
src/components/FooterMoreBadges/badges.json | 50 ++++++++++
src/components/FooterMoreBadges/index.js | 90 +++++++++++++++++
src/components/FooterMoreBadges/stars.json | 13 +++
src/components/FooterMoreBadges/styles.module.css | 115 +++++++++++++++++++++
src/css/custom.css | 40 --------
src/theme/Footer/Links/MultiColumn/index.js | 116 ++++++++++++++++++++++
static/img/footer/github.svg | 3 +
static/img/footer/x.svg | 3 +
13 files changed, 489 insertions(+), 106 deletions(-)
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index c768916f..6c0091a4 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -48,6 +48,9 @@ jobs:
if: github.event_name != 'pull_request'
run: yarn crowdin:sync
+ - name: Generate footer stars
+ run: yarn generate:footer-stars
+
- name: Build
run: yarn build
diff --git a/.github/workflows/pr_ci.yml b/.github/workflows/pr_ci.yml
index 8c68a579..3024e6ce 100644
--- a/.github/workflows/pr_ci.yml
+++ b/.github/workflows/pr_ci.yml
@@ -22,5 +22,8 @@ jobs:
- name: Install dependencies
run: yarn install
+ - name: Generate footer stars
+ run: yarn generate:footer-stars
+
- name: Build website
run: yarn build --locale en
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 862884dc..bd2e0f8c 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -234,72 +234,8 @@ module.exports = {
},
{
title: "More",
- items: [
- {
- html: `
- <a href="https://github.com/casbin/casbin" target="_blank">
- <img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/casbin/casbin?label=Casbin&style=social">
- </a>
-
- <a href="https://github.com/casbin/jcasbin" target="_blank">
- <img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/casbin/jcasbin?label=jCasbin&style=social">
- </a>
- `,
- },
- {
- html: `
- <a href="https://github.com/casbin/node-casbin"
target="_blank">
- <img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/casbin/node-casbin?label=Node-Casbin&style=social">
- </a>
-
- <a href="https://github.com/php-casbin/php-casbin"
target="_blank">
- <img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/php-casbin/php-casbin?label=PHP-Casbin&style=social">
- </a>
- `,
- },
- {
- html: `
- <a href="https://github.com/casbin/pycasbin" target="_blank">
- <img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/casbin/pycasbin?label=PyCasbin&style=social">
- </a>
-
- <a href="https://github.com/casbin/Casbin.NET" target="_blank">
- <img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/casbin/Casbin.NET?label=Casbin.NET&style=social">
- </a>
- `,
- },
- {
- html: `
- <a href="https://github.com/casbin/casbin-cpp" target="_blank">
- <img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/casbin/casbin-cpp?label=Casbin-CPP&style=social">
- </a>
-
- <a href="https://github.com/casbin/casbin-rs" target="_blank">
- <img alt="GitHub Repo stars"
src="https://img.shields.io/github/stars/casbin/casbin-rs?label=Casbin-RS&style=social">
- </a>
- `,
- },
- {
- html: `
- <a href="https://twitter.com/casbinHQ" target="_blank">
- <img alt="Twitter Follow"
src="https://img.shields.io/twitter/follow/casbinHQ?style=social">
- </a>
- `,
- },
- {
- html: `
-<script>
-var _hmt = _hmt || [];
-(function() {
- var hm = document.createElement("script");
- hm.src = "https://hm.baidu.com/hm.js?2c0ffc6f8d49e98d964d59d7aa4cbf34";
- var s = document.getElementsByTagName("script")[0];
- s.parentNode.insertBefore(hm, s);
-})();
-</script>
-`,
- },
- ],
+ className: "footer-more-column",
+ items: [],
},
],
logo: {
diff --git a/package.json b/package.json
index 153182a1..d63b6281 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
+ "generate:footer-stars": "node scripts/generate-footer-stars.js",
"start": "node scripts/start.js",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
diff --git a/scripts/generate-footer-stars.js b/scripts/generate-footer-stars.js
new file mode 100644
index 00000000..0ab1a51a
--- /dev/null
+++ b/scripts/generate-footer-stars.js
@@ -0,0 +1,90 @@
+const fs = require("fs");
+const path = require("path");
+
+const badges = require("../src/components/FooterMoreBadges/badges.json");
+
+const outputPath = path.join(
+ __dirname,
+ "..",
+ "src",
+ "components",
+ "FooterMoreBadges",
+ "stars.json"
+);
+
+function getRepoKey(owner, repo) {
+ return `${owner}/${repo}`;
+}
+
+function readExistingStars() {
+ if (!fs.existsSync(outputPath)) {
+ return null;
+ }
+
+ try {
+ return JSON.parse(fs.readFileSync(outputPath, "utf8"));
+ } catch {
+ return null;
+ }
+}
+
+async function fetchStars() {
+ const headers = {
+ Accept: "application/vnd.github+json",
+ "User-Agent": "casbin-website-footer-stars",
+ };
+
+ if (process.env.GITHUB_TOKEN) {
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
+ }
+
+ const results = await Promise.all(
+ badges.map(async({owner, repo}) => {
+ const response = await
fetch(`https://api.github.com/repos/${owner}/${repo}`, {
+ headers,
+ });
+
+ if (!response.ok) {
+ const body = await response.text();
+ throw new Error(`Failed to fetch stars for ${owner}/${repo}:
${response.status} ${body}`);
+ }
+
+ const data = await response.json();
+
+ if (typeof data.stargazers_count !== "number") {
+ throw new Error(`Missing stargazers_count for ${owner}/${repo}`);
+ }
+
+ return [getRepoKey(owner, repo), data.stargazers_count];
+ })
+ );
+
+ return Object.fromEntries(results);
+}
+
+async function main() {
+ const existingStars = readExistingStars();
+
+ try {
+ const stars = await fetchStars();
+ const payload = {
+ generatedAt: new Date().toISOString(),
+ stars,
+ };
+
+ fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`);
+ process.stdout.write(`Updated footer stars at ${outputPath}\n`);
+ } catch (error) {
+ if (existingStars?.stars) {
+ process.stderr.write(`${error.message}\nUsing existing footer stars
JSON.\n`);
+ return;
+ }
+
+ throw error;
+ }
+}
+
+main().catch((error) => {
+ process.stderr.write(`${error.stack || error}\n`);
+ process.exit(1);
+});
diff --git a/src/components/FooterMoreBadges/badges.json
b/src/components/FooterMoreBadges/badges.json
new file mode 100644
index 00000000..d26952cd
--- /dev/null
+++ b/src/components/FooterMoreBadges/badges.json
@@ -0,0 +1,50 @@
+[
+ {
+ "label": "Casbin",
+ "owner": "casbin",
+ "repo": "casbin",
+ "href": "https://github.com/casbin/casbin"
+ },
+ {
+ "label": "jCasbin",
+ "owner": "casbin",
+ "repo": "jcasbin",
+ "href": "https://github.com/casbin/jcasbin"
+ },
+ {
+ "label": "Node-Casbin",
+ "owner": "casbin",
+ "repo": "node-casbin",
+ "href": "https://github.com/casbin/node-casbin"
+ },
+ {
+ "label": "PHP-Casbin",
+ "owner": "php-casbin",
+ "repo": "php-casbin",
+ "href": "https://github.com/php-casbin/php-casbin"
+ },
+ {
+ "label": "PyCasbin",
+ "owner": "casbin",
+ "repo": "pycasbin",
+ "href": "https://github.com/casbin/pycasbin"
+ },
+ {
+ "label": "Casbin.NET",
+ "owner": "casbin",
+ "repo": "Casbin.NET",
+ "href": "https://github.com/casbin/Casbin.NET"
+ },
+ {
+ "label": "Casbin-CPP",
+ "owner": "casbin",
+ "repo": "casbin-cpp",
+ "href": "https://github.com/casbin/casbin-cpp"
+ },
+ {
+ "label": "Casbin-RS",
+ "owner": "casbin",
+ "repo": "casbin-rs",
+ "href": "https://github.com/casbin/casbin-rs"
+ }
+]
diff --git a/src/components/FooterMoreBadges/index.js
b/src/components/FooterMoreBadges/index.js
new file mode 100644
index 00000000..41210819
--- /dev/null
+++ b/src/components/FooterMoreBadges/index.js
@@ -0,0 +1,90 @@
+import React from "react";
+import styles from "./styles.module.css";
+import githubBadges from "./badges.json";
+import starsData from "./stars.json";
+
+const GITHUB_BADGES = githubBadges;
+const STAR_COUNTS = starsData.stars ?? {};
+const STAR_FORMATTER = new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ compactDisplay: "short",
+ maximumFractionDigits: 1,
+});
+
+function getRepoKey(owner, repo) {
+ return `${owner}/${repo}`;
+}
+
+function formatStars(count) {
+ return STAR_FORMATTER.format(count).replace(/\.0(?=[A-Za-z])/u,
"").toLowerCase();
+}
+
+function BadgeLink({href, ariaLabel, children}) {
+ return (
+ <a
+ className={`footer__link-item ${styles.badgeLink}`}
+ href={href}
+ target="_blank"
+ rel="noopener noreferrer"
+ aria-label={ariaLabel}
+ >
+ {children}
+ </a>
+ );
+}
+
+function GitHubStarBadge({label, stars}) {
+ const safeStars = Number.isFinite(stars) ? stars : 0;
+ const formattedStars = formatStars(safeStars);
+
+ return (
+ <span className={styles.badgeSurface} aria-hidden="true">
+ <span className={styles.badgeLabelSection}>
+ <img className={styles.badgeIcon} src="/img/footer/github.svg" alt=""
/>
+ <span className={styles.badgeText}>{label}</span>
+ </span>
+ <span className={styles.badgeDivider} />
+ <span className={styles.badgeCountSection}>{formattedStars}</span>
+ </span>
+ );
+}
+
+function XFollowBadge() {
+ return (
+ <span className={`${styles.badgeSurface} ${styles.followBadge}`}
aria-hidden="true">
+ <img className={styles.badgeIcon} src="/img/footer/x.svg" alt="" />
+ <span className={styles.badgeText}>Follow @casbinHQ</span>
+ </span>
+ );
+}
+
+function GitHubBadgeLink({badge, stars}) {
+ const {label, href} = badge;
+
+ return (
+ <BadgeLink href={href} ariaLabel={`${label} GitHub repository`}>
+ <GitHubStarBadge label={label} stars={stars} />
+ </BadgeLink>
+ );
+}
+
+function XBadgeLink() {
+ return (
+ <BadgeLink href="https://twitter.com/casbinHQ" ariaLabel="Follow @casbinHQ
on X">
+ <XFollowBadge />
+ </BadgeLink>
+ );
+}
+
+export default function FooterMoreBadges() {
+ return (
+ <div className={styles.badgesGrid}>
+ {GITHUB_BADGES.map((badge) => {
+ const repoKey = getRepoKey(badge.owner, badge.repo);
+
+ return <GitHubBadgeLink key={repoKey} badge={badge}
stars={STAR_COUNTS[repoKey]} />;
+ })}
+ <XBadgeLink />
+ </div>
+ );
+}
diff --git a/src/components/FooterMoreBadges/stars.json
b/src/components/FooterMoreBadges/stars.json
new file mode 100644
index 00000000..04d95b5a
--- /dev/null
+++ b/src/components/FooterMoreBadges/stars.json
@@ -0,0 +1,13 @@
+{
+ "generatedAt": "2026-04-13T13:15:04.436Z",
+ "stars": {
+ "casbin/casbin": 19992,
+ "casbin/jcasbin": 2631,
+ "casbin/node-casbin": 2882,
+ "php-casbin/php-casbin": 1326,
+ "casbin/pycasbin": 1724,
+ "casbin/Casbin.NET": 1309,
+ "casbin/casbin-cpp": 250,
+ "casbin/casbin-rs": 1090
+ }
+}
diff --git a/src/components/FooterMoreBadges/styles.module.css
b/src/components/FooterMoreBadges/styles.module.css
new file mode 100644
index 00000000..c8e3b0cd
--- /dev/null
+++ b/src/components/FooterMoreBadges/styles.module.css
@@ -0,0 +1,115 @@
+.badgesGrid {
+ display: grid;
+ grid-template-columns: repeat(2, max-content);
+ gap: 0.5rem 1rem;
+ justify-content: start;
+}
+
+.badgeLink {
+ display: inline-flex;
+ line-height: 0;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.badgeSurface {
+ display: inline-flex;
+ align-items: stretch;
+ width: 135px;
+ height: 20px;
+ overflow: hidden;
+ border: 1px solid #d5d5d5;
+ border-radius: 2px;
+ background: #fcfcfc;
+ color: #333;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 11px;
+ font-weight: 700;
+ line-height: 1;
+ transition: filter 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ will-change: filter;
+}
+
+.badgeLabelSection {
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 6px;
+ width: 96px;
+ padding: 0 6px;
+ box-sizing: border-box;
+}
+
+.badgeDivider {
+ position: relative;
+ width: 7px;
+ background: #fafafa;
+ border-left: 1px solid #d5d5d5;
+}
+
+.badgeDivider::before {
+ content: "";
+ position: absolute;
+ left: -1px;
+ top: 6px;
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ border-left: 4px solid #fafafa;
+}
+
+.badgeCountSection {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ background: #fafafa;
+}
+
+.badgeIcon {
+ width: 14px;
+ height: 14px;
+ flex-shrink: 0;
+}
+
+.badgeText {
+ display: inline-block;
+ min-width: 0;
+ white-space: nowrap;
+ text-align: left;
+}
+
+.followBadge {
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 0 10px;
+ box-sizing: border-box;
+}
+
+.badgeLink:hover {
+ transform: translateY(-3px) scale(1.08) rotate(2deg);
+}
+
+.badgeLink:hover .badgeSurface {
+ filter: drop-shadow(0 6px 12px rgb(59 130 246 / 35%)) brightness(1.15);
+}
+
+.badgeLink:active {
+ transform: translateY(-1px) scale(1.02);
+ transition-duration: 0.1s;
+}
+
+.badgeLink:active .badgeSurface {
+ filter: drop-shadow(0 2px 4px rgb(59 130 246 / 25%)) brightness(1.1);
+}
+
+.badgeLink:focus-visible {
+ outline: 2px solid var(--ifm-color-primary-lightest);
+ outline-offset: 3px;
+ border-radius: 4px;
+}
+
+@media only screen and (max-width: 996px) {
+ .badgesGrid {
+ grid-template-columns: max-content;
+ }
+}
diff --git a/src/css/custom.css b/src/css/custom.css
index ef1bc28e..2a543430 100644
--- a/src/css/custom.css
+++ b/src/css/custom.css
@@ -272,46 +272,6 @@ html[data-theme="dark"] .theme-doc-sidebar-container
div[title="Expand sidebar"]
padding-top: 20px;
}
-/* Footer "More" section button animations */
-footer a[href*="github.com"],
-footer a[href*="twitter.com"] {
- display: inline-block;
- position: relative;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-/* Subtle animation for footer images */
-footer a[href*="github.com"] img,
-footer a[href*="twitter.com"] img {
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- will-change: transform, filter;
-}
-
-/* Hover effects for footer buttons - simple glow and lift */
-footer a[href*="github.com"]:hover,
-footer a[href*="twitter.com"]:hover {
- transform: translateY(-3px) scale(1.08) rotate(2deg);
-}
-
-footer a[href*="github.com"]:hover img,
-footer a[href*="twitter.com"]:hover img {
- filter: drop-shadow(0 6px 12px rgb(59 130 246 / 35%)) brightness(1.15);
- transform: scale(1.05);
-}
-
-/* Active/Click state with bounce effect */
-footer a[href*="github.com"]:active,
-footer a[href*="twitter.com"]:active {
- transform: translateY(-1px) scale(1.02);
- transition: all 0.1s ease;
-}
-
-footer a[href*="github.com"]:active img,
-footer a[href*="twitter.com"]:active img {
- transform: scale(0.95);
- filter: drop-shadow(0 2px 4px rgb(59 130 246 / 25%)) brightness(1.1);
-}
-
/* Footer Community section icons */
.footer-community-link {
display: inline-flex;
diff --git a/src/theme/Footer/Links/MultiColumn/index.js
b/src/theme/Footer/Links/MultiColumn/index.js
new file mode 100644
index 00000000..a935742f
--- /dev/null
+++ b/src/theme/Footer/Links/MultiColumn/index.js
@@ -0,0 +1,116 @@
+import React from "react";
+import clsx from "clsx";
+import {ThemeClassNames} from "@docusaurus/theme-common";
+import LinkItem from "@theme/Footer/LinkItem";
+import FooterMoreBadges from "@site/src/components/FooterMoreBadges";
+
+function getShortHash(value) {
+ let hash = 0;
+
+ for (let index = 0; index < value.length; index += 1) {
+ hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
+ }
+
+ return hash.toString(36);
+}
+
+function getItemKey(item) {
+ if (item.id) {
+ return item.id;
+ }
+
+ const stableKey = [
+ item.href,
+ item.to,
+ item.label,
+ item.className,
+ ].filter(Boolean).join("::");
+
+ if (stableKey) {
+ return stableKey;
+ }
+
+ if (item.html) {
+ return `html:${getShortHash(item.html)}`;
+ }
+
+ return "item";
+}
+
+function getColumnKey(column) {
+ return [
+ column.title,
+ column.className,
+ ...column.items.map(getItemKey),
+ ].filter(Boolean).join("::");
+}
+
+function ColumnLinkItem({item}) {
+ return item.html ? (
+ <li
+ className={clsx("footer__item", item.className)}
+ // Developer provided the HTML, so assume it's safe.
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{__html: item.html}}
+ />
+ ) : (
+ <li key={item.href ?? item.to} className="footer__item">
+ <LinkItem item={item} />
+ </li>
+ );
+}
+
+function CustomColumnItem({item}) {
+ return item.html ? (
+ <div
+ className={item.className}
+ // Developer provided the HTML, so assume it's safe.
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{__html: item.html}}
+ />
+ ) : (
+ <div className="footer__item">
+ <LinkItem item={item} />
+ </div>
+ );
+}
+
+function Column({column}) {
+ const isFooterMoreColumn =
column.className?.split(/\s+/u).includes("footer-more-column");
+
+ return (
+ <div
+ className={clsx(
+ ThemeClassNames.layout.footer.column,
+ "col footer__col",
+ column.className
+ )}
+ >
+ <div className="footer__title">{column.title}</div>
+ {isFooterMoreColumn ? (
+ <>
+ <FooterMoreBadges />
+ {column.items.map((item) => (
+ <CustomColumnItem key={getItemKey(item)} item={item} />
+ ))}
+ </>
+ ) : (
+ <ul className="footer__items clean-list">
+ {column.items.map((item) => (
+ <ColumnLinkItem key={getItemKey(item)} item={item} />
+ ))}
+ </ul>
+ )}
+ </div>
+ );
+}
+
+export default function FooterLinksMultiColumn({columns}) {
+ return (
+ <div className="row footer__links">
+ {columns.map((column) => (
+ <Column key={getColumnKey(column)} column={column} />
+ ))}
+ </div>
+ );
+}
diff --git a/static/img/footer/github.svg b/static/img/footer/github.svg
new file mode 100644
index 00000000..43b3ec5f
--- /dev/null
+++ b/static/img/footer/github.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
+ <path fill="#181717" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8
8.205 11.385.6.113.82-.258.82-.577
0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633
17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236
1.07 1.835 2.809 1.305
3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93
0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3
1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2 [...]
+</svg>
diff --git a/static/img/footer/x.svg b/static/img/footer/x.svg
new file mode 100644
index 00000000..bea4b6f7
--- /dev/null
+++ b/static/img/footer/x.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
+ <path fill="#000" d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251
0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837
3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.186z"/>
+</svg>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]