diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index c768916f9..6c0091a4a 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 8c68a5795..3024e6cec 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 fb91717f2..518e4524e 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -234,72 +234,8 @@ module.exports = {
},
{
title: "More",
- items: [
- {
- html: `
-
-
-
-
-
-
-
- `,
- },
- {
- html: `
-
-
-
-
-
-
-
- `,
- },
- {
- html: `
-
-
-
-
-
-
-
- `,
- },
- {
- html: `
-
-
-
-
-
-
-
- `,
- },
- {
- html: `
-
-
-
- `,
- },
- {
- html: `
-
-`,
- },
- ],
+ className: "footer-more-column",
+ items: [],
},
],
logo: {
diff --git a/package.json b/package.json
index 153182a17..d63b6281e 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 000000000..0ab1a51a0
--- /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 000000000..d26952cd5
--- /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 000000000..412108190
--- /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 (
+
+ {children}
+
+ );
+}
+
+function GitHubStarBadge({label, stars}) {
+ const safeStars = Number.isFinite(stars) ? stars : 0;
+ const formattedStars = formatStars(safeStars);
+
+ return (
+
+ );
+}
+
+function XFollowBadge() {
+ return (
+
+ );
+}
+
+function GitHubBadgeLink({badge, stars}) {
+ const {label, href} = badge;
+
+ return (
+