搜索 K
主题
@vaultkit/vitepress-plugin-remote-file 提供 ::: remote-file 容器:构建期抓取 GitHub / CNB 仓库中的文件内容直接嵌入文章,免复制粘贴,随每次部署自动同步。嵌入产物带出处 chrome(平台徽标 + 可点路径链接 + ref / 行号徽标),代码本体走站点自己的 shiki 管线——高亮主题、行号、行高亮 meta 全部天然可用。
它是系列中架构最讲究的一个:两段协作——remoteFile() Vite 插件负责抓取与原文改写,remoteFileFrame() markdown-it 容器负责出处 chrome,缺一不可。本文覆盖三种 URL 形态、全部 7 个容器属性、全部 5 个插件 options,并讲透两段架构的必然性。
技术文章引用上游仓库代码,复制粘贴是万恶之源:上游改了你不知道,文章里的代码悄悄过时;而「贴个链接让读者自己去看」又打断阅读流。remote-file 取中道——构建期抓取真实文件内容嵌进页面,出处链接与行号徽标保留可溯源性,每次部署自动与上游同步。
pnpm add -D @vaultkit/vitepress-plugin-remote-file// .vitepress/config.ts **两段都要注册**,位置不同
import { remoteFile, remoteFileFrame } from '@vaultkit/vitepress-plugin-remote-file'
import { defineConfig } from 'vitepress'
export default defineConfig({
vite: { plugins: [remoteFile()] }, // 第一段:Vite 插件
markdown: {
config(md) {
md.use(remoteFileFrame) // 第二段:markdown-it 容器
},
},
})// .vitepress/theme/index.ts
import '@vaultkit/vitepress-plugin-remote-file/styles/index.css'使用 @vaultkit/vitepress-theme-vault 主题则开箱即带,容器直接可用。
主参数走 info-string 位,单行即闭合、::: 收尾可省:
::: remote-file https://github.com/vuejs/vitepress/blob/main/package.json {lines="1-15"}实活效果(本页构建期真实抓取 VitePress 仓库的 package.json 前 15 行):
{
"name": "vitepress",
"version": "2.0.0-alpha.18",
"description": "Vite & Vue powered static site generator",
"keywords": [
"vite",
"vue",
"vitepress"
],
"homepage": "https://vitepress.dev/",
"bugs": {
"url": "https://github.com/vuejs/vitepress/issues"
},
"repository": "github:vuejs/vitepress",
"license": "MIT",平台从 URL 自动识别,全部支持 #L10-L40 行区间片段:
日常从浏览器地址栏复制的形态,分支名或 commit SHA(permalink,文件页按 y 获取)都行,refs/heads/ 前缀自动归一:
::: remote-file https://github.com/vuejs/vitepress/blob/main/src/shared/shared.ts#L1-L16import type {
AdditionalConfig,
HeadConfig,
PageData,
SiteData
} from '../../types/shared'
export type {
AdditionalConfig,
AdditionalConfigDict,
AdditionalConfigLoader,
Awaitable,
DefaultTheme,
HeadConfig,
Header,
LocaleConfig,raw.githubusercontent.com 形态同样直接可用:
::: remote-file https://raw.githubusercontent.com/vuejs/vitepress/main/package.json{
"name": "vitepress",
"version": "2.0.0-alpha.18",
"description": "Vite & Vue powered static site generator",
"keywords": [
"vite",
"vue",
"vitepress"
],
"homepage": "https://vitepress.dev/",
"bugs": {
"url": "https://github.com/vuejs/vitepress/issues"
},
"repository": "github:vuejs/vitepress",
"license": "MIT",
"author": "Evan You",
"type": "module",
"exports": {
".": {
"types": "./types/index.d.ts",
"default": "./dist/node/index.js"
},
"./dist/*": "./dist/*",
"./package.json": "./package.json",
"./client": {
"types": "./client.d.ts",
"default": "./dist/client/index.js"
},
"./theme": {
"types": "./theme.d.ts",
"default": "./dist/client/theme-default/index.js"
},
"./theme-without-fonts": {
"types": "./theme-without-fonts.d.ts",
"default": "./dist/client/theme-default/without-fonts.js"
}
},
"main": "dist/node/index.js",
"types": "types/index.d.ts",
"bin": {
"vitepress": "bin/vitepress.js"
},
"files": [
"bin",
"dist",
"types",
"template",
"client.d.ts",
"theme.d.ts",
"theme-without-fonts.d.ts",
"lib"
],
"scripts": {
"clean": "node -e \"require('node:fs').rmSync('./dist',{recursive:!0,force:!0,maxRetries:10})\"",
"dev": "pnpm clean && pnpm dev:shared && pnpm dev:start",
"dev:start": "pnpm --stream '/^dev:(client|node|watch)$/'",
"dev:client": "tsc --sourcemap -w --preserveWatchOutput -p src/client",
"dev:node": "DEV=true pnpm build:node -w",
"dev:shared": "node scripts/copyShared.ts",
"dev:watch": "node scripts/watchAndCopy.ts",
"build": "pnpm build:prepare && pnpm build:client && pnpm build:node",
"build:prepare": "pnpm clean && node scripts/copyShared.ts",
"build:client": "vue-tsc --noEmit -p src/client && tsc -p src/client && node scripts/copyClient.ts",
"build:node": "tsc -p src/node --noEmit && rollup --config rollup.config.ts --configPlugin esbuild",
"test": "pnpm --aggregate-output --reporter=append-only '/^test:(unit|e2e|init)$/'",
"test:unit": "vitest run -r __tests__/unit",
"test:unit:watch": "vitest -r __tests__/unit",
"test:e2e": "pnpm test:e2e-dev && pnpm test:e2e-build",
"test:e2e:site:dev": "pnpm -F=tests-e2e site:dev",
"test:e2e:site:build": "pnpm -F=tests-e2e site:build",
"test:e2e:site:preview": "pnpm -F=tests-e2e site:preview",
"test:e2e-dev": "pnpm -F=tests-e2e test",
"test:e2e-dev:watch": "pnpm -F=tests-e2e watch",
"test:e2e-build": "VITE_TEST_BUILD=1 pnpm test:e2e-dev",
"test:e2e-build:watch": "VITE_TEST_BUILD=1 pnpm test:e2e-dev:watch",
"test:init": "pnpm -F=tests-init test",
"test:init:watch": "pnpm -F=tests-init watch",
"docs": "pnpm --stream '/^(docs:)?dev$/'",
"docs:dev": "wait-on -d 100 dist/node/cli.js && pnpm -F=docs dev",
"docs:debug": "NODE_OPTIONS='--inspect-brk' pnpm docs:dev",
"docs:build": "pnpm build && pnpm docs:build:only",
"docs:build:only": "pnpm -F=docs build",
"docs:preview": "pnpm -F=docs preview",
"docs:lunaria:build": "pnpm -F=docs lunaria:build",
"docs:lunaria:open": "pnpm -F=docs lunaria:open",
"format": "prettier --experimental-cli --write .",
"format:fail": "prettier --experimental-cli --check .",
"check": "pnpm format:fail && pnpm build && pnpm test",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"release": "node scripts/release.ts"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "prettier --experimental-cli --ignore-unknown --write"
},
"dependencies": {
"@docsearch/css": "^4.6.3",
"@docsearch/js": "^4.6.3",
"@docsearch/sidepanel-js": "^4.6.3",
"@iconify-json/simple-icons": "^1.2.87",
"@shikijs/core": "^4.3.0",
"@shikijs/transformers": "^4.3.0",
"@shikijs/types": "^4.3.0",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^6.0.7",
"@vue/devtools-api": "^8.1.4",
"@vue/shared": "^3.5.39",
"@vueuse/core": "^14.3.0",
"@vueuse/integrations": "^14.3.0",
"focus-trap": "^8.2.2",
"mark.js": "8.11.1",
"minisearch": "^7.2.0",
"shiki": "^4.3.0",
"vite": "^8.1.3",
"vue": "^3.5.39"
},
"devDependencies": {
"@clack/prompts": "^1.6.0",
"@emnapi/core": "^1.11.1",
"@emnapi/runtime": "^1.11.1",
"@iconify/utils": "^3.1.3",
"@mdit-vue/plugin-component": "^3.0.2",
"@mdit-vue/plugin-frontmatter": "^3.0.2",
"@mdit-vue/plugin-headers": "^3.0.2",
"@mdit-vue/plugin-sfc": "^3.0.2",
"@mdit-vue/plugin-title": "^3.0.2",
"@mdit-vue/plugin-toc": "^3.0.2",
"@mdit-vue/shared": "^3.0.2",
"@polka/compression": "^1.0.0-next.28",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@types/cross-spawn": "^6.0.6",
"@types/lodash.template": "^4.5.3",
"@types/mark.js": "^8.11.12",
"@types/markdown-it-attrs": "^4.1.3",
"@types/markdown-it-container": "^4.0.0",
"@types/markdown-it-emoji": "^3.0.1",
"@types/minimist": "^1.2.5",
"@types/node": "^25.9.4",
"@types/picomatch": "^4.0.3",
"@types/semver": "^7.7.1",
"chokidar": "^5.0.0",
"conventional-changelog": "^7.2.1",
"conventional-changelog-angular": "^8.3.1",
"cross-spawn": "^7.0.6",
"esbuild": "^0.27.7",
"get-port": "^7.2.0",
"gray-matter": "^4.0.3",
"image-size": "^2.0.2",
"lint-staged": "^16.4.0",
"lodash.template": "^4.18.1",
"lru-cache": "^11.5.1",
"markdown-it": "^14.2.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-async": "^2.2.0",
"markdown-it-attrs": "4.3.1",
"markdown-it-cjk-friendly": "^2.0.2",
"markdown-it-container": "^4.0.0",
"markdown-it-emoji": "^3.0.0",
"markdown-it-mathjax3": "^4.3.2",
"minimist": "^1.2.8",
"nanoid": "^5.1.16",
"obug": "^2.1.3",
"ora": "^9.4.1",
"p-map": "^7.0.4",
"package-directory": "^8.2.0",
"path-to-regexp": "^6.3.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.4",
"playwright-chromium": "^1.61.1",
"polka": "^1.0.0-next.28",
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.4",
"prettier": "^3.9.0",
"punycode": "^2.3.1",
"rollup": "^4.62.2",
"rollup-plugin-dts": "6.1.1",
"rollup-plugin-esbuild": "^6.2.1",
"semver": "^7.8.5",
"simple-git-hooks": "^2.13.1",
"sirv": "^3.0.2",
"sitemap": "^9.0.1",
"tinyglobby": "^0.2.17",
"typescript": "^5.9.3",
"vitest": "^4.1.9",
"vue-tsc": "^3.3.5",
"wait-on": "^9.0.10"
},
"peerDependencies": {
"markdown-it-mathjax3": "^4",
"postcss": "^8"
},
"peerDependenciesMeta": {
"markdown-it-mathjax3": {
"optional": true
},
"postcss": {
"optional": true
}
},
"packageManager": "pnpm@10.33.0"
}cnb.cool 的 /-/blob/(及 /-/raw/、/-/git/raw/)形态,仓库路径可含多级组织;构建需要 CNB_TOKEN 环境变量:
::: remote-file https://cnb.cool/examples/showcase/-/blob/main/README.md {lines="1-10"}# CNB完整示例


## NPC
1. [Claude Code](https://cnb.cool/npc/claude-code)
2. [Open Code](https://cnb.cool/npc/open-code)
3. [HOK - 王者荣耀英雄氛围编程助手](https://cnb.cool/npc/hok)全部 7 个属性(repo / ref / path 为一组显式坐标):
| 属性 | 类型 | 缺省 | 说明 |
|---|---|---|---|
url | string | — | 文件 URL(等价于 info-string 主参数位) |
lines | string | URL 的 #L… 片段 | 行区间,"10-40" 或单行 "5" |
lang | string | 按扩展名推断 | 覆盖 shiki 语言 |
title | string | 文件名 | 代码块标题栏文案 |
meta | string | — | 透传围栏 meta |
repo / path | string | — | CNB 旧语法的显式坐标 |
ref | string | main | 显式坐标的分支 / SHA |
文件地址的属性写法,与 info-string 主参数完全等价(主参数优先):
::: remote-file {url="https://github.com/vuejs/vitepress/blob/main/package.json"}行区间截取,优先级高于 URL 的 #L… 片段;"10-40" 取区间,"5" 取单行。截取结果同步显示为出处栏的行号徽标(L10-L40):
::: remote-file https://github.com/vuejs/vitepress/blob/main/package.json {lines="1-15"}覆盖代码高亮语言。缺省按文件扩展名推断,内置一批别名(mjs/cjs → js、mts/cts → ts、yml → yaml、dockerfile → docker、makefile → make),无扩展名文件落 txt。推断不准时手工指定:
::: remote-file https://github.com/vuejs/vitepress/blob/main/.prettierrc {lang="json"}代码块标题栏文案,缺省取文件名:
::: remote-file https://github.com/vuejs/vitepress/blob/main/package.json {title="VitePress 的包清单"}原样透传给生成的代码围栏 meta 位——站点 shiki 管线认识什么它就支持什么,最常用的是行高亮:
::: remote-file https://github.com/vuejs/vitepress/blob/main/package.json {lines="1-15" meta="{2-3}"}CNB 旧语法的显式坐标形态(不给 URL 时生效),ref 缺省 main:
::: remote-file {repo="examples/showcase" ref="main" path="README.md"}另有两个兼容别名容器 ::: github-file / ::: cnb-file,构建期打弃用告警,下个大版本移除——新文章一律用 ::: remote-file <url>。
remoteFile(options) 共 5 项(remoteFileFrame(md) 无选项):
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
githubToken | string | 环境变量 GITHUB_TOKEN | GitHub 令牌,可选(仅提额) |
cnbToken | string | 环境变量 CNB_TOKEN | CNB 令牌,CNB 文件必需 |
cnbApiBase | string | 'https://api.cnb.cool' | CNB API 基址 |
ttl | number | 3_600_000 | 缓存有效期(ms,缺省 1 小时) |
cacheDir | string | node_modules/.cache/vitepress-plugin-remote-file | 缓存目录(CI 建议持久化) |
remoteFile({
ttl: 24 * 3_600_000,
cacheDir: '.cache/remote-file',
})第一段 remoteFile()(Vite 插件,enforce: 'pre'):markdown-it 的解析规则是同步的,没法 await fetch()——md 编译之前的 Vite transform 是唯一干净的异步时机。它抓取文件内容,把容器改写为「remote-file-frame 容器 + 普通代码围栏」的组合(示意,行首空格为围栏保护):
::: remote-file-frame {platform="github" path="vuejs/vitepress/package.json" ref="main" href="…" lines="L1-L15"}
```json [package.json]
…抓取到的文件内容…
```
:::关键抉择在这里:改写为普通代码围栏而不是自渲染 HTML。这样代码本体走站点自己的 shiki 管线,高亮主题、行号、文件名标题栏、meta 行高亮全部天然可用——插件不重复造高亮的轮子。
第二段 remoteFileFrame()(markdown-it 容器):改写产物里的 frame 容器需要有人渲染成出处 chrome(平台徽标 + 路径链接 + ref / 行号徽标,data-platform 切换 GitHub / CNB 视觉)。这一步必须发生在 markdown-it 管线内——因为它要包住已被 shiki 处理过的代码块。
少注册第一段,容器原样留在页面上;少注册第二段,改写出的 frame 容器没人认识。所以二者缺一不可。
嵌入的文件内容本身可能含三反引号围栏串(比如嵌一个 Markdown 文件)。插件扫描内容里最长的行首反引号串,把包裹围栏加长到比它多一根——CommonMark 规定围栏必须长于内容中的任意围栏标记,任意文件内容都不破版。
与 repo-card 同款(复用 @vaultkit/repo-platform-kit):TTL 内直接命中磁盘缓存;过期重抓失败则回退陈缓存并告警;既无网又无缓存才 fail-fast。上游删文件、断网、限额都不会让旧文章突然构建失败。
分支形态的 URL(blob/main/...)里,上游一旦改动文件,行号立即漂移。需要长期稳定的行区间时,用 commit SHA 固定链接(GitHub 文件页按 y 得 permalink)——代价是不再跟随上游更新,按引用意图二选一。
嵌入内容什么时候更新? 构建时缓存过期即重抓。想强制刷新,删缓存目录再构建。
能嵌私有仓库吗? 能,GITHUB_TOKEN / CNB_TOKEN 有权限即可——但注意内容会被固化进公开页面。
围栏里能顶格演示 ::: remote-file 语法吗? 能。第一段的原文改写会先按 CommonMark 围栏规则切段、只处理围栏外的文本,代码块里的示例原样保留。