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>