This is an automated email from the ASF dual-hosted git repository.

twice pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/kvrocks-website.git


The following commit(s) were added to refs/heads/main by this push:
     new d71d60e4 Fetch avatars while yarn build to workaround CSP rule (#362)
d71d60e4 is described below

commit d71d60e44ba6023588234b3f2dec19e1f3b280af
Author: Twice <[email protected]>
AuthorDate: Tue Mar 17 00:37:56 2026 +0800

    Fetch avatars while yarn build to workaround CSP rule (#362)
    
    * Fetch avatars while yarn build to workaround CSP rule
    
    * fix
    
    * fix
---
 .gitignore                          |   2 +
 blog/authors.yml                    |  17 ---
 docusaurus.config.js                |   1 +
 package.json                        |   5 +-
 scripts/sync-github-avatars.js      | 175 +++++++++++++++++++++++++++++++
 src/components/Committers/index.tsx |  48 +++------
 src/data/people.json                | 204 ++++++++++++++++++++++++++++++++++++
 static/img/avatar-placeholder.svg   |   5 +
 8 files changed, 404 insertions(+), 53 deletions(-)

diff --git a/.gitignore b/.gitignore
index 53ea5189..641d7c16 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,8 @@
 # Generated files
 .docusaurus
 .cache-loader
+/blog/authors.generated.yml
+/static/generated
 
 # Misc
 .DS_Store
diff --git a/blog/authors.yml b/blog/authors.yml
deleted file mode 100644
index 5a122d93..00000000
--- a/blog/authors.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-hulk:
-  name: Hulk Lin
-  title: Apache Kvrocks PMC Member
-  url: https://github.com/git-hulk
-  image_url: https://github.com/git-hulk.png
-
-vmihailenco:
-  name: Vladimir Mihailenco
-  title: Grumpy Gopher
-  url: https://github.com/vmihailenco
-  image_url: https://github.com/vmihailenco.png
-
-twice:
-  name: PragmaTwice
-  title: Apache Kvrocks PMC Member
-  url: https://github.com/pragmatwice
-  image_url: https://github.com/pragmatwice.png
diff --git a/docusaurus.config.js b/docusaurus.config.js
index a1ae75ca..b01c9482 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -53,6 +53,7 @@ const config = {
         },
         blog: {
           showReadingTime: true,
+          authorsMapPath: 'authors.generated.yml',
           editUrl: 'https://github.com/apache/kvrocks-website/tree/main/',
         },
         theme: {
diff --git a/package.json b/package.json
index a2f9e68b..8516ecb6 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,10 @@
   "version": "0.0.0",
   "private": true,
   "scripts": {
+    "sync:avatars": "node scripts/sync-github-avatars.js",
     "docusaurus": "docusaurus",
-    "start": "docusaurus start",
-    "build": "docusaurus build",
+    "start": "yarn sync:avatars && docusaurus start",
+    "build": "yarn sync:avatars && docusaurus build",
     "swizzle": "docusaurus swizzle",
     "deploy": "docusaurus deploy",
     "clear": "docusaurus clear",
diff --git a/scripts/sync-github-avatars.js b/scripts/sync-github-avatars.js
new file mode 100644
index 00000000..2cc2863c
--- /dev/null
+++ b/scripts/sync-github-avatars.js
@@ -0,0 +1,175 @@
+#!/usr/bin/env node
+
+const fs = require("fs/promises");
+const path = require("path");
+const http = require("http");
+const https = require("https");
+
+const people = require("../src/data/people.json");
+
+const rootDir = path.resolve(__dirname, "..");
+const avatarDir = path.join(rootDir, "static/generated/avatars/github");
+const generatedAuthorsPath = path.join(rootDir, "blog/authors.generated.yml");
+const placeholderAvatarPath = "/img/avatar-placeholder.svg";
+const forceRefresh = /^(1|true)$/i.test(process.env.FORCE_SYNC_AVATARS || "");
+
+function avatarPublicPath(githubId) {
+  return `/generated/avatars/github/${githubId}.png`;
+}
+
+function getGithubIds() {
+  const ids = new Set();
+
+  for (const committer of people.committers) {
+    ids.add(committer.githubId);
+  }
+
+  for (const author of Object.values(people.blogAuthors)) {
+    if (author.githubId) {
+      ids.add(author.githubId);
+    }
+  }
+
+  return [...ids].sort((left, right) => left.localeCompare(right));
+}
+
+async function exists(filePath) {
+  try {
+    await fs.access(filePath);
+    return true;
+  } catch {
+    return false;
+  }
+}
+
+function download(url, redirects = 0) {
+  if (redirects > 5) {
+    return Promise.reject(new Error(`Too many redirects for ${url}`));
+  }
+
+  const client = url.startsWith("https:") ? https : http;
+
+  return new Promise((resolve, reject) => {
+    const request = client.get(
+      url,
+      {
+        headers: {
+          "user-agent": "kvrocks-website-avatar-sync",
+        },
+      },
+      (response) => {
+        const { statusCode = 0, headers } = response;
+
+        if (statusCode >= 300 && statusCode < 400 && headers.location) {
+          response.resume();
+          const nextUrl = new URL(headers.location, url).toString();
+          resolve(download(nextUrl, redirects + 1));
+          return;
+        }
+
+        if (statusCode !== 200) {
+          response.resume();
+          reject(new Error(`Unexpected status ${statusCode} for ${url}`));
+          return;
+        }
+
+        const chunks = [];
+        response.on("data", (chunk) => chunks.push(chunk));
+        response.on("end", () => resolve(Buffer.concat(chunks)));
+      }
+    );
+
+    request.on("error", reject);
+  });
+}
+
+async function syncAvatar(githubId) {
+  const filePath = path.join(avatarDir, `${githubId}.png`);
+
+  if (!forceRefresh && (await exists(filePath))) {
+    return { publicPath: avatarPublicPath(githubId), source: "cache" };
+  }
+
+  try {
+    const avatar = await 
download(`https://github.com/${githubId}.png?size=128`);
+    await fs.writeFile(filePath, avatar);
+    return { publicPath: avatarPublicPath(githubId), source: "download" };
+  } catch (error) {
+    if (await exists(filePath)) {
+      return { publicPath: avatarPublicPath(githubId), source: "cache" };
+    }
+
+    return {
+      publicPath: placeholderAvatarPath,
+      source: "placeholder",
+      error,
+    };
+  }
+}
+
+function stringifyYamlValue(value) {
+  return JSON.stringify(value);
+}
+
+function buildAuthorBlock(author, imageURL) {
+  const lines = [];
+
+  if (author.name) {
+    lines.push(`  name: ${stringifyYamlValue(author.name)}`);
+  }
+  if (author.title) {
+    lines.push(`  title: ${stringifyYamlValue(author.title)}`);
+  }
+  if (author.url || author.githubId) {
+    lines.push(
+      `  url: ${stringifyYamlValue(author.url || 
`https://github.com/${author.githubId}`)}`
+    );
+  }
+  lines.push(`  image_url: ${stringifyYamlValue(imageURL)}`);
+
+  return lines.join("\n");
+}
+
+async function writeGeneratedAuthors(avatarStates) {
+  const entries = Object.entries(people.blogAuthors).sort(([left], [right]) =>
+    left.localeCompare(right)
+  );
+  const content = [
+    "# This file is generated by scripts/sync-github-avatars.js.",
+    "# Do not edit it directly.",
+    "",
+    ...entries.map(([key, author]) => {
+      const imageURL = author.githubId
+        ? avatarStates.get(author.githubId)?.publicPath || 
placeholderAvatarPath
+        : placeholderAvatarPath;
+
+      return `${key}:\n${buildAuthorBlock(author, imageURL)}`;
+    }),
+    "",
+  ].join("\n");
+
+  await fs.writeFile(generatedAuthorsPath, content);
+}
+
+async function main() {
+  await fs.mkdir(avatarDir, { recursive: true });
+
+  const avatarStates = new Map();
+  for (const githubId of getGithubIds()) {
+    const state = await syncAvatar(githubId);
+    avatarStates.set(githubId, state);
+
+    if (state.source === "placeholder") {
+      console.warn(
+        `[avatar-sync] Falling back to placeholder for ${githubId}: 
${state.error.message}`
+      );
+    }
+  }
+
+  await writeGeneratedAuthors(avatarStates);
+}
+
+main().catch((error) => {
+  console.error("[avatar-sync] Failed to prepare avatars", error);
+  process.exitCode = 1;
+});
diff --git a/src/components/Committers/index.tsx 
b/src/components/Committers/index.tsx
index da2390e1..4cdbe62b 100644
--- a/src/components/Committers/index.tsx
+++ b/src/components/Committers/index.tsx
@@ -1,6 +1,8 @@
 import React from "react"
 import styles from "./index.module.css"
 
+const people = require("@site/src/data/people.json");
+
 type CommitterData = {
     name: string,
     apacheId: string,
@@ -8,39 +10,12 @@ type CommitterData = {
     isPMC: boolean,
 }
 
-// sorted by apacheId
-const committers: CommitterData[] = [
-    {name: 'Aleks Lozoviuk', apacheId: 'aleksraiden', githubId: 'aleksraiden', 
isPMC: true},
-    {name: 'Donghui Liu', apacheId: 'alfejik', githubId: 'Alfejik', isPMC: 
true},
-    {name: 'Beihao Zhou', apacheId: 'beihao', githubId: 'Beihao-Zhou', isPMC: 
false},
-    {name: 'Binbin Zhu', apacheId: 'binbin', githubId: 'enjoy-binbin', isPMC: 
false},
-    {name: 'Brad Lee', apacheId: 'bradlee', githubId: 'smartlee', isPMC: 
false},
-    {name: 'Pengbo Cai', apacheId: 'caipengbo', githubId: 'caipengbo', isPMC: 
true},
-    {name: 'Liang Chen', apacheId: 'chenliang613', githubId: 'chenliang613', 
isPMC: true},
-    {name: 'Chris Zou', apacheId: 'chriszou', githubId: 'ChrisZMF', isPMC: 
false},
-    {name: 'Colin Chamber', apacheId: 'colinchamber', githubId: 
'ColinChamber', isPMC: false},
-    {name: 'Edward Xu', apacheId: 'edwardxu', githubId: 'LindaSummer', isPMC: 
false},
-    {name: 'Xiaoqiao He', apacheId: 'hexiaoqiao', githubId: 'Hexiaoqiao', 
isPMC: true},
-    {name: 'Hulk Lin', apacheId: 'hulk', githubId: 'git-hulk', isPMC: true},
-    {name: 'Jean-Baptiste Onofré', apacheId: 'jbonofre', githubId: 'jbonofre', 
isPMC: true},
-    {name: 'Jean Lai', apacheId: 'jeanbone', githubId: 'jeanbone', isPMC: 
false},
-    {name: 'Ji Huayu', apacheId: 'jihuayu', githubId: 'jihuayu', isPMC: false},
-    {name: 'Miuyong Liu', apacheId: 'karelrooted', githubId: 'karelrooted', 
isPMC: false},
-    {name: 'Xuwei Fu', apacheId: 'maplefu', githubId: 'mapleFU', isPMC: true},
-    {name: 'Shang Xiong', apacheId: 'shang', githubId: 'shangxiaoxiong', 
isPMC: false},
-    {name: 'SiLe Zhou', apacheId: 'silezhou', githubId: 'PokIsemaine', isPMC: 
false},
-    {name: 'Xiaojun Yuan', apacheId: 'sryanyuan', githubId: 'sryanyuan', 
isPMC: false},
-    {name: 'Ruixiang Tan', apacheId: 'tanruixiang', githubId: 'tanruixiang', 
isPMC: false},
-    {name: 'Zili Chen', apacheId: 'tison', githubId: 'tisonkun', isPMC: true},
-    {name: 'Yaroslav Stepanchuk', apacheId: 'torwig', githubId: 'torwig', 
isPMC: true},
-    {name: 'Mingyang Liu', apacheId: 'twice', githubId: 'PragmaTwice', isPMC: 
true},
-    {name: 'Von Gosling', apacheId: 'vongosling', githubId: 'vongosling', 
isPMC: true},
-    {name: 'Yuan Wang', apacheId: 'wangyuan', githubId: 'ShooterIT', isPMC: 
true},
-    {name: 'Xiaobiao Zhao', apacheId: 'xiaobiao', githubId: 'xiaobiaozhao', 
isPMC: false},
-    {name: 'Shixi Yang', apacheId: 'yangshixi', githubId: 'Yangsx-1', isPMC: 
false},
-    {name: 'Agnik Misra', apacheId: 'agnik', githubId: 'Jitmisra', isPMC: 
false},
-    {name: 'Rongxing Xiao', apacheId: 'deemo', githubId: 'yezhizi', isPMC: 
false}
-]
+const committers: CommitterData[] = people.committers;
+const placeholderAvatar = "/img/avatar-placeholder.svg";
+
+function avatarPath(githubId: string): string {
+    return `/generated/avatars/github/${githubId}.png`;
+}
 
 export default function Committers(): JSX.Element {
     return <>
@@ -59,7 +34,12 @@ export default function Committers(): JSX.Element {
                 .map(v => (
                     <tr key={v.name}>
                         <td><img width={64} 
className={styles.contributorAvatar}
-                                 src={`https://github.com/${v.githubId}.png`} 
alt={v.name}/></td>
+                                 src={avatarPath(v.githubId)}
+                                 onError={(event) => {
+                                     event.currentTarget.onerror = null;
+                                     event.currentTarget.src = 
placeholderAvatar;
+                                 }}
+                                 alt={v.name}/></td>
                         <td>{v.isPMC ? <b>{v.name}</b> : v.name}</td>
                         <td>{v.apacheId}</td>
                         <td><a target={"_blank"} 
href={`https://github.com/${v.githubId}`}>{v.githubId}</a></td>
diff --git a/src/data/people.json b/src/data/people.json
new file mode 100644
index 00000000..a624f15a
--- /dev/null
+++ b/src/data/people.json
@@ -0,0 +1,204 @@
+{
+  "blogAuthors": {
+    "hulk": {
+      "name": "Hulk Lin",
+      "title": "Apache Kvrocks PMC Member",
+      "url": "https://github.com/git-hulk";,
+      "githubId": "git-hulk"
+    },
+    "vmihailenco": {
+      "name": "Vladimir Mihailenco",
+      "title": "Grumpy Gopher",
+      "url": "https://github.com/vmihailenco";,
+      "githubId": "vmihailenco"
+    },
+    "twice": {
+      "name": "PragmaTwice",
+      "title": "Apache Kvrocks PMC Member",
+      "url": "https://github.com/pragmatwice";,
+      "githubId": "pragmatwice"
+    }
+  },
+  "committers": [
+    {
+      "name": "Aleks Lozoviuk",
+      "apacheId": "aleksraiden",
+      "githubId": "aleksraiden",
+      "isPMC": true
+    },
+    {
+      "name": "Donghui Liu",
+      "apacheId": "alfejik",
+      "githubId": "Alfejik",
+      "isPMC": true
+    },
+    {
+      "name": "Agnik Misra",
+      "apacheId": "agnik",
+      "githubId": "Jitmisra",
+      "isPMC": false
+    },
+    {
+      "name": "Beihao Zhou",
+      "apacheId": "beihao",
+      "githubId": "Beihao-Zhou",
+      "isPMC": false
+    },
+    {
+      "name": "Binbin Zhu",
+      "apacheId": "binbin",
+      "githubId": "enjoy-binbin",
+      "isPMC": false
+    },
+    {
+      "name": "Brad Lee",
+      "apacheId": "bradlee",
+      "githubId": "smartlee",
+      "isPMC": false
+    },
+    {
+      "name": "Pengbo Cai",
+      "apacheId": "caipengbo",
+      "githubId": "caipengbo",
+      "isPMC": true
+    },
+    {
+      "name": "Liang Chen",
+      "apacheId": "chenliang613",
+      "githubId": "chenliang613",
+      "isPMC": true
+    },
+    {
+      "name": "Chris Zou",
+      "apacheId": "chriszou",
+      "githubId": "ChrisZMF",
+      "isPMC": false
+    },
+    {
+      "name": "Colin Chamber",
+      "apacheId": "colinchamber",
+      "githubId": "ColinChamber",
+      "isPMC": false
+    },
+    {
+      "name": "Rongxing Xiao",
+      "apacheId": "deemo",
+      "githubId": "yezhizi",
+      "isPMC": false
+    },
+    {
+      "name": "Edward Xu",
+      "apacheId": "edwardxu",
+      "githubId": "LindaSummer",
+      "isPMC": false
+    },
+    {
+      "name": "Xiaoqiao He",
+      "apacheId": "hexiaoqiao",
+      "githubId": "Hexiaoqiao",
+      "isPMC": true
+    },
+    {
+      "name": "Hulk Lin",
+      "apacheId": "hulk",
+      "githubId": "git-hulk",
+      "isPMC": true
+    },
+    {
+      "name": "Jean-Baptiste Onofré",
+      "apacheId": "jbonofre",
+      "githubId": "jbonofre",
+      "isPMC": true
+    },
+    {
+      "name": "Jean Lai",
+      "apacheId": "jeanbone",
+      "githubId": "jeanbone",
+      "isPMC": false
+    },
+    {
+      "name": "Ji Huayu",
+      "apacheId": "jihuayu",
+      "githubId": "jihuayu",
+      "isPMC": false
+    },
+    {
+      "name": "Miuyong Liu",
+      "apacheId": "karelrooted",
+      "githubId": "karelrooted",
+      "isPMC": false
+    },
+    {
+      "name": "Xuwei Fu",
+      "apacheId": "maplefu",
+      "githubId": "mapleFU",
+      "isPMC": true
+    },
+    {
+      "name": "Shang Xiong",
+      "apacheId": "shang",
+      "githubId": "shangxiaoxiong",
+      "isPMC": false
+    },
+    {
+      "name": "SiLe Zhou",
+      "apacheId": "silezhou",
+      "githubId": "PokIsemaine",
+      "isPMC": false
+    },
+    {
+      "name": "Xiaojun Yuan",
+      "apacheId": "sryanyuan",
+      "githubId": "sryanyuan",
+      "isPMC": false
+    },
+    {
+      "name": "Ruixiang Tan",
+      "apacheId": "tanruixiang",
+      "githubId": "tanruixiang",
+      "isPMC": false
+    },
+    {
+      "name": "Zili Chen",
+      "apacheId": "tison",
+      "githubId": "tisonkun",
+      "isPMC": true
+    },
+    {
+      "name": "Yaroslav Stepanchuk",
+      "apacheId": "torwig",
+      "githubId": "torwig",
+      "isPMC": true
+    },
+    {
+      "name": "Mingyang Liu",
+      "apacheId": "twice",
+      "githubId": "PragmaTwice",
+      "isPMC": true
+    },
+    {
+      "name": "Von Gosling",
+      "apacheId": "vongosling",
+      "githubId": "vongosling",
+      "isPMC": true
+    },
+    {
+      "name": "Yuan Wang",
+      "apacheId": "wangyuan",
+      "githubId": "ShooterIT",
+      "isPMC": true
+    },
+    {
+      "name": "Xiaobiao Zhao",
+      "apacheId": "xiaobiao",
+      "githubId": "xiaobiaozhao",
+      "isPMC": false
+    },
+    {
+      "name": "Shixi Yang",
+      "apacheId": "yangshixi",
+      "githubId": "Yangsx-1",
+      "isPMC": false
+    }
+  ]
+}
diff --git a/static/img/avatar-placeholder.svg 
b/static/img/avatar-placeholder.svg
new file mode 100644
index 00000000..bc8c606e
--- /dev/null
+++ b/static/img/avatar-placeholder.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 64 64" role="img" 
aria-label="Avatar placeholder">
+  <rect width="64" height="64" rx="32" fill="#D8DEE9"/>
+  <circle cx="32" cy="24" r="12" fill="#8F9BB3"/>
+  <path d="M14 56c2-10 10-16 18-16s16 6 18 16" fill="#8F9BB3"/>
+</svg>

Reply via email to