Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/content-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,12 @@ jobs:
cache: "pnpm"

- run: pnpm install --frozen-lockfile
# Non-blocking image migration + lint (visibility only)
- name: Migrate images next to MDX (check only)
run: pnpm migrate:images || echo "[warn] migrate:images failed (non-blocking)"

- name: Lint image references (non-blocking)
run: pnpm lint:images || echo "[warn] image lint found issues (non-blocking)"

# Build the site to validate MDX and docs using Fumadocs
- run: pnpm build
10 changes: 10 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
# 1) 将 /images/* 文章图片就近复制并更新引用
pnpm migrate:images || exit 1

# 将迁移后的变更加入暂存,确保本次提交包含更新
git add -A

# 2) 校验图片路径与命名(不合规则阻止提交)
pnpm lint:images || exit 1

# 3) 其余按 lint-staged 处理(如 Prettier)
pnpm exec lint-staged
29 changes: 14 additions & 15 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ touch docs/computer-science/data-structures/new-topic/index.mdx

使用 Markdown/MDX 编写文章:

````mdx
```mdx
---
title: "文章标题"
description: "文章简短描述"
Expand All @@ -51,28 +51,16 @@ tags:
更多内容...

## 代码示例

```javascript
// 你的代码
function example() {
return "Hello World!";
}
```
````

## 总结

文章总结...

````

### 步骤4:测试修改

使用 Fumadocs 验证内容:

```bash
pnpm build
````
pnpm migrate:images # 迁移图片脚本
```

此命令将:

Expand Down Expand Up @@ -141,6 +129,17 @@ npm dev
## 📚 文档规范

所有文档放在 `docs/` 目录。
图片需要放在 被引用的文档的同名`assets`目录下(正常情况下您不应该关心这个, 该项目有自动脚本来移动图片), 例如:
docxA 引用了 imgA 图片, 那么他们的文档结构应该是 `docxA.assets/imgA`:

```md
docsA.mdx
docsA.assets/
imgA
```

![img](public/readme_docs_structure.png)

每个文档都需要一个 Frontmatter,例如:

```md
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,17 @@ pnpm dev
pnpm dev # 启动开发服务器
pnpm build # 构建生产版本
pnpm start # 启动生产服务器
pnpm postinstall
pnpm lint:images # 检查图片符合规则
pnpm migrate:images # 迁移图片
```

# 内容
## 图片管理规范(简要)

自动化脚本会移动您引用的图片到 MDX 同目录下, 遵循以下规则:

# 导出
pnpm export # 导出静态站点到 /out 目录
```
- 存放:与 MDX 同目录的 `./<basename>.assets/` 中。
- 例:`foo.mdx` → `./foo.assets/<img>.png`;`index.mdx` → `./index.assets/<img>.png`。
- 引用:相对路径 `![](./<basename>.assets/<img>.png)`。
- 自动化:提交时自动迁移并改引用;图片 Lint 只提示不拦截提交。
- 共享:站点级用 `/images/site/*`、组件演示用 `/images/components/<name>/*`;多文档共用的图片可保留 `/images/...`。
8 changes: 4 additions & 4 deletions app/docs/ai/multimodal/llava/index.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
---
title: "LLaVA"
description: "LLaVA多模态大模型框架:架构解析、CLIP基础、论文精读、复现实践"
date: "2025-01-27"
Expand All @@ -12,7 +12,7 @@ tags:

LLaVA (Large Language and Vision Assistant) 是多模态大模型的开创性框架,开启了视觉指令调优的新范式。

![](/images/word/word-img-03.png)
![](index.assets/word-img-03.png)

## 核心架构

Expand All @@ -22,9 +22,9 @@ LLaVA (Large Language and Vision Assistant) 是多模态大模型的开创性框
ViT视觉编码器 → 投影层跨模态对齐 → LLM语言生成
```

![](/images/word/word-img-04.png)
![](index.assets/word-img-04.png)

![](/images/word/word-img-05.png)
![](index.assets/word-img-05.png)

### 技术特点

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"build": "next build",
"start": "next start",
"postinstall": "fumadocs-mdx",
"prepare": "husky"
"prepare": "husky",
"lint:images": "node scripts/check-images.mjs",
"migrate:images": "node scripts/move-doc-images.mjs"
},
"dependencies": {
"@types/mdx": "^2.0.13",
Expand Down
Binary file added public/readme_docs_structure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
200 changes: 200 additions & 0 deletions scripts/check-images.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#!/usr/bin/env node
/**
* MDX 图片路径校验脚本
*
* 功能
* - 扫描 `app/docs/??/?.mdx`(含 .md)
* - 识别 Markdown `![]()` 与内联 `<img src="…" />` 的图片引用
* - 强制使用“就近图片”:推荐相对路径(如 `./images/…`)
* - 仅对站点级共享资源允许绝对路径:`/images/site/*`、`/images/components/*`
* - 校验图片文件是否存在、文件名是否符合 kebab-case
*
* 目的
* - 图片与文章同目录,便于维护与迁移
* - 避免全局命名冲突,降低重构成本
*
* 使用
* - 包脚本:`pnpm lint:images`
* - 直接运行:`node scripts/check-images.mjs`
*
* 退出码
* - 0:通过;1:存在问题(便于接入 CI)
*/

import fs from "fs";
import path from "path";

const ROOT = process.cwd();
const DOCS_DIR = path.join(ROOT, "app", "docs");
// 允许的绝对路径前缀(站点级 & 组件演示级别)
const ALLOWED_ABSOLUTE_PREFIXES = ["/images/site/", "/images/components/"];

// 图片文件扩展名
const IMAGE_FILE_EXTS = new Set([
".png",
".jpg",
".jpeg",
".gif",
".webp",
".svg",
]);
const exts = new Set([".mdx", ".md"]);

/** Recursively list files */
function* walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
yield* walk(p);
} else {
yield p;
}
}
}

/**
* 将文件路径转换为路由路径
*/
function toRoutePath(file) {
const rel = path.relative(DOCS_DIR, file).split(path.sep).join("/");
const base = path.basename(rel);
const dir = path.dirname(rel);
if (base.toLowerCase() === "index.mdx") return dir === "." ? "" : dir;
const name = base.replace(/\.[^.]+$/, "");
return dir === "." ? name : `${dir}/${name}`;
}

/**
* 判断文件名(含扩展名)是否为 kebab-case(仅校验主名,不含后缀)
* 示例:training-loop.png、fig-01-architecture.webp 为合格
*/
function isKebabCase(name) {
// allow dot for extension only
const base = name.replace(/\.[^.]+$/, "");
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(base);
}

/**
* 校验单个 MD(X) 文件中的图片使用:
* - 非站点级的绝对路径 `/images/*` 会被提示应改为相对路径
* - 相对路径需位于同一文档目录(避免越级引用)
* - 检查文件是否存在与命名是否符合 kebab-case
*/
/**
* 构建全局引用表:统计所有文档对非站点级 `/images/...` 的引用次数
*/
function buildRefs() {
const reMdx = /!\[[^\]]*\]\(([^)]+)\)/g;
const reHtml = /<img[^>]*src=["']([^"']+)["'][^>]*>/gi;
/** @type {Map<string, Set<string>>} */
const refs = new Map();
for (const f of walk(DOCS_DIR)) {
if (!exts.has(path.extname(f))) continue;
const s = fs.readFileSync(f, "utf8");
const urls = new Set();
for (const m of s.matchAll(reMdx)) urls.add(m[1]);
for (const m of s.matchAll(reHtml)) urls.add(m[1]);
for (const url of urls) {
const u = url.replace(/\\/g, "/");
if (!u.startsWith("/images/")) continue;
if (ALLOWED_ABSOLUTE_PREFIXES.some((p) => u.startsWith(p))) continue;
if (!refs.has(u)) refs.set(u, new Set());
refs.get(u).add(f);
}
}
return refs;
}

function checkFile(file, refs) {
const content = fs.readFileSync(file, "utf8");
const routePath = toRoutePath(file);
const baseDir = path.dirname(file);
const baseName = path.basename(file, path.extname(file));
const expectedRelPrefix = `./${baseName}.assets/`;
// Markdown 图片语法:![alt](src)
const re = /!\[[^\]]*\]\(([^)]+)\)/g;
// HTML 图片语法:<img src="..." />
const inlineRe = /<img[^>]*src=["']([^"']+)["'][^>]*>/gi;
const problems = [];

function checkUrl(url, loc) {
const urlNorm = url.replace(/\\/g, "/");
if (/^https?:\/\//i.test(urlNorm)) return; // 外链忽略
// 绝对路径
if (urlNorm.startsWith("/images/")) {
const okAbs =
ALLOWED_ABSOLUTE_PREFIXES.some((p) => urlNorm.startsWith(p)) ||
(refs.get(urlNorm)?.size || 0) > 1;
if (!okAbs) {
problems.push(
`${loc}: prefer co-located images; use ${expectedRelPrefix}<file> (avoid ${urlNorm})`,
);
}
const fname = urlNorm.split("/").pop() || "";
if (!isKebabCase(fname))
problems.push(`${loc}: filename not kebab-case -> ${fname}`);
return;
}
// 相对路径
if (urlNorm.startsWith("./") || urlNorm.startsWith("../")) {
const abs = path.resolve(baseDir, urlNorm);
const relToDocs = path.relative(DOCS_DIR, abs);
if (relToDocs.startsWith("..")) {
problems.push(
`${loc}: image must live within the same doc folder (got ${urlNorm})`,
);
}
const ext = path.extname(abs).toLowerCase();
if (!IMAGE_FILE_EXTS.has(ext)) return; // 非图片文件忽略
if (!fs.existsSync(abs)) {
problems.push(`${loc}: image file not found -> ${urlNorm}`);
}
const fname = path.basename(abs);
if (!isKebabCase(fname))
problems.push(`${loc}: filename not kebab-case -> ${fname}`);
return;
}
// 其它形式(如 data: 或 import)忽略
}

// scan markdown image syntax
for (const m of content.matchAll(re)) {
checkUrl(m[1], "mdx");
}
// scan inline <img>
for (const m of content.matchAll(inlineRe)) {
checkUrl(m[1], "html");
}

return problems;
}

function main() {
if (!fs.existsSync(DOCS_DIR)) {
console.error(`Docs dir not found: ${DOCS_DIR}`);
process.exit(1);
}
let total = 0;
let bad = 0;
const refs = buildRefs();
for (const f of walk(DOCS_DIR)) {
if (!exts.has(path.extname(f))) continue;
total++;
const probs = checkFile(f, refs);
if (probs.length) {
bad++;
const rel = path.relative(ROOT, f).split(path.sep).join("/");
console.log(`\n${rel}`);
for (const p of probs) console.log(` - ${p}`);
}
}
if (bad) {
console.log(`\nFound ${bad}/${total} files with image issues.`);
process.exit(1);
} else {
console.log(`Checked ${total} MDX files: no issues.`);
}
}

main();
Loading