Skip to content

Commit ecb7e75

Browse files
committed
feat: serve lessons from local mirror
1 parent c9c90a8 commit ecb7e75

File tree

4 files changed

+105
-34
lines changed

4 files changed

+105
-34
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ yarn-error.log*
1515
dist/
1616
build/
1717
_book/
18+
public/content/
1819

1920
# Environment files
2021
.env

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"description": "Modern React-powered Web3 learning platform with interactive tutorials and AI assistance",
55
"type": "module",
66
"scripts": {
7+
"predev": "npm run sync-content",
8+
"prebuild": "npm run sync-content",
9+
"sync-content": "node scripts/sync-content.mjs",
710
"dev": "vite",
811
"build": "vite build",
912
"preview": "vite preview",

scripts/sync-content.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { cp, mkdir, rm, stat } from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = path.dirname(__filename);
7+
const projectRoot = path.resolve(__dirname, '..');
8+
9+
const SOURCE_ROOT = path.join(projectRoot, 'zh');
10+
const DEST_ROOT = path.join(projectRoot, 'public', 'content', 'zh');
11+
12+
const SOURCE_FOLDERS = [
13+
'Web3QuickStart',
14+
'GetStartedWithBitcoin',
15+
'Web3Thoughts',
16+
];
17+
18+
const ensureExists = async (dir) => {
19+
await mkdir(dir, { recursive: true });
20+
};
21+
22+
const safeCopy = async (source, destination) => {
23+
try {
24+
const statResult = await stat(source);
25+
if (!statResult.isDirectory()) {
26+
console.warn(`[sync-content] Skip ${source}: not a directory`);
27+
return;
28+
}
29+
} catch (error) {
30+
console.warn(`[sync-content] Skip ${source}: ${error.message}`);
31+
return;
32+
}
33+
34+
await ensureExists(path.dirname(destination));
35+
await rm(destination, { recursive: true, force: true });
36+
await cp(source, destination, { recursive: true });
37+
console.log(`[sync-content] Copied ${source} -> ${destination}`);
38+
};
39+
40+
const main = async () => {
41+
await ensureExists(path.join(projectRoot, 'public', 'content'));
42+
43+
for (const folder of SOURCE_FOLDERS) {
44+
const src = path.join(SOURCE_ROOT, folder);
45+
const dest = path.join(DEST_ROOT, folder);
46+
await safeCopy(src, dest);
47+
}
48+
49+
console.log(`[sync-content] Completed. Content root: ${path.relative(projectRoot, DEST_ROOT)}`);
50+
};
51+
52+
main().catch((error) => {
53+
console.error('[sync-content] Failed:', error);
54+
process.exitCode = 1;
55+
});
56+

src/App.jsx

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,15 @@ const GITHUB_BRANCH = import.meta.env.VITE_GITHUB_BRANCH || "main";
273273
const getRawBaseUrl = (path) => `https://raw.githubusercontent.com/${GITHUB_USERNAME}/${GITHUB_REPO}/${GITHUB_BRANCH}/${path}/`;
274274
const getRawUrl = (path) => `https://raw.githubusercontent.com/${GITHUB_USERNAME}/${GITHUB_REPO}/${GITHUB_BRANCH}/${path}`;
275275

276+
const ensureTrailingSlash = (value = '') => value.endsWith('/') ? value : `${value}/`;
277+
const stripLeadingSlash = (value = '') => value.replace(/^\/+/, '');
278+
279+
const APP_BASE_URL = ensureTrailingSlash(import.meta.env.BASE_URL || '/');
280+
const LOCAL_CONTENT_BASE = ensureTrailingSlash(`${APP_BASE_URL}content`);
281+
282+
const getLocalBaseUrl = (path = '') => ensureTrailingSlash(`${LOCAL_CONTENT_BASE}${stripLeadingSlash(path)}`);
283+
const getLocalUrl = (path = '') => `${LOCAL_CONTENT_BASE}${stripLeadingSlash(path)}`;
284+
276285
const COURSE_DATA = [
277286
{
278287
id: 'module-1',
@@ -2886,49 +2895,51 @@ export default function App() {
28862895
if (!activeLesson.path) {
28872896
setLessonContent(activeLesson.fallbackContent);
28882897
setFetchError(null);
2898+
setBasePath('');
28892899
return;
28902900
}
28912901

28922902
const fetchContent = async () => {
28932903
setContentLoading(true);
28942904
setFetchError(null);
2895-
setBasePath(getRawBaseUrl(activeLesson.path));
2905+
setBasePath('');
28962906

2897-
const tryFetch = async (filename) => {
2898-
const path = `${activeLesson.path}/${filename}`;
2899-
const url = getRawUrl(path);
2900-
try {
2901-
const res = await fetch(url);
2902-
if (res.ok) {
2903-
return await res.text();
2904-
}
2905-
return null;
2906-
} catch (e) {
2907-
return null;
2907+
const candidateFiles = ['README.MD', 'README.md', 'readme.md', 'index.md', 'index.MD'];
2908+
const failureLogs = [];
2909+
const sources = [
2910+
{
2911+
label: '本地内容镜像',
2912+
basePath: getLocalBaseUrl(activeLesson.path),
2913+
getUrl: (filename) => getLocalUrl(`${activeLesson.path}/${filename}`)
2914+
},
2915+
{
2916+
label: 'GitHub Raw',
2917+
basePath: getRawBaseUrl(activeLesson.path),
2918+
getUrl: (filename) => getRawUrl(`${activeLesson.path}/${filename}`)
29082919
}
2909-
};
2920+
];
29102921

29112922
try {
2912-
let text = await tryFetch('README.MD');
2913-
if (!text) {
2914-
console.log("README.MD not found, trying README.md...");
2915-
text = await tryFetch('README.md');
2916-
}
2917-
if (!text) {
2918-
console.log("README.md not found, trying readme.md...");
2919-
text = await tryFetch('readme.md');
2923+
for (const source of sources) {
2924+
for (const filename of candidateFiles) {
2925+
const url = source.getUrl(filename);
2926+
try {
2927+
const res = await fetch(url, { cache: 'no-store' });
2928+
if (res.ok) {
2929+
const text = await res.text();
2930+
setLessonContent(text);
2931+
setBasePath(source.basePath);
2932+
return;
2933+
}
2934+
failureLogs.push(`${source.label}: ${url} (${res.status})`);
2935+
} catch (error) {
2936+
failureLogs.push(`${source.label}: ${url} (${error.message})`);
2937+
}
2938+
}
29202939
}
29212940

2922-
if (text) {
2923-
setLessonContent(text);
2924-
} else {
2925-
const failedUrl = getRawUrl(`${activeLesson.path}/README.MD`);
2926-
console.error(`Failed to fetch from ${failedUrl}`);
2927-
setFetchError(failedUrl);
2928-
setLessonContent(activeLesson.fallbackContent);
2929-
}
2930-
} catch (err) {
2931-
console.error(err);
2941+
console.error('Failed to load lesson content', failureLogs);
2942+
setFetchError(failureLogs.join('\n'));
29322943
setLessonContent(activeLesson.fallbackContent);
29332944
} finally {
29342945
setContentLoading(false);
@@ -3462,14 +3473,14 @@ export default function App() {
34623473
{contentLoading ? (
34633474
<div className="flex flex-col items-center justify-center py-20 text-slate-400 gap-4">
34643475
<Loader2 className="w-10 h-10 text-cyan-400 animate-spin" />
3465-
<p>正在从 GitHub 抓取最新教程...</p>
3476+
<p>正在加载课程内容...</p>
34663477
</div>
34673478
) : fetchError ? (
34683479
<div className="p-6 bg-red-900/20 border border-red-500/30 rounded-xl text-center">
34693480
<div className="flex justify-center mb-4"><AlertTriangle className="w-8 h-8 text-red-400" /></div>
34703481
<h3 className="text-lg font-bold text-red-400 mb-2">内容加载失败</h3>
3471-
<p className="text-slate-400 text-sm mb-4">无法从 GitHub 获取文件。请检查仓库路径是否正确</p>
3472-
<div className="bg-black/30 p-3 rounded font-mono text-xs text-slate-500 break-all">{fetchError}</div>
3482+
<p className="text-slate-400 text-sm mb-4">无法加载课程文本,已尝试本地镜像与 GitHub Raw</p>
3483+
<div className="bg-black/30 p-3 rounded font-mono text-xs text-slate-500 break-all whitespace-pre-wrap">{fetchError}</div>
34733484
</div>
34743485
) : (
34753486
<article className="prose prose-invert prose-lg max-w-3xl mx-auto mb-12">

0 commit comments

Comments
 (0)