很长一段时间里,分享 GitHub 代码就意味着截图,或把原始片段粘贴进 Markdown。这两种方式都不太可靠:截图让 RSS 阅读器看不到文本,复制粘贴的代码又会在上游文件一改动时失去同步。我想要 Emgithub 的可读性、服务端渲染带来的 SEO 优势,以及完全不依赖第三方 JavaScript。
这周,这几项终于凑齐了:一个 {% github %} 短代码,在构建时获取代码,完成高亮、行号和复制按钮。它只需要 GitHub blob URL,以及一个可选的明暗界面样式参数。
目标和约束
在让 GPT Codex 写代码之前,我先定了几条边界:
- 适合构建时处理。 所有内容都应在 Eleventy 构建期间完成,让生成的 HTML 已经包含代码,对搜索引擎、RSS 和离线阅读器都友好。
- 缓存远程请求。 反复获取 GitHub 原始文件会很慢,因此由 EleventyFetch 处理缓存。
- 保持简单的写作体验。 短代码应接受大家熟悉的 GitHub “blob” URL,包括可选的
#L10-L42 行范围片段标识。
- 契合网站外观。 我借鉴了 Emgithub 的视觉语言:上方是文件信息,下方是代码行;再用自己的 CSS 融入 Subspace 主题。
解析 GitHub URL
第一个组成部分是一个小解析器,将 GitHub blob URL 拆解成可用的信息。它提取用户、仓库、分支、文件路径,以及 URL 片段标识中的行范围。
const parseBlobUrl = (githubBlobUrl) => {
const url = new URL(githubBlobUrl);
const parts = url.pathname.split('/').filter(Boolean);
if (parts[2] !== 'blob') throw new Error('URL must be a GitHub blob URL');
const [user, repo, , branch, ...fileParts] = parts;
const filePath = fileParts.join('/');
const rangeHash = (url.hash || '').replace(/^#/, '');
let start = null;
let end = null;
if (rangeHash.startsWith('L')) {
const [first, last] = rangeHash
.split('-')
.map((part) => part.replace(/^L/, ''));
start = parseInt(first, 10);
end = last ? parseInt(last, 10) : start;
}
const raw = `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${filePath}`;
return {
user,
repo,
branch,
filePath,
raw,
web: githubBlobUrl,
start,
end,
};
};
没有片段标识时,短代码会渲染整个文件。传入包含 #L8-L25 的 URL,就只渲染这些行;GitHub 自带的永久链接界面让获取这种 URL 很方便。
在构建时获取并高亮
取得元数据后,EleventyFetch 会在构建期间获取原始文件内容。结果缓存 24 小时,让增量构建保持快速。
const source = typeof fetched === 'string' ? fetched : String(fetched);
let code = source;
if (meta.start && meta.end) {
const lines = source.split('\n');
code = lines.slice(meta.start - 1, meta.end).join('\n');
}
接着由 Highlight.js 高亮代码。我根据文件扩展名识别语言;当 highlight.js 不认识该语法时,就退回纯文本。
const language = guessLanguageByExt(meta.filePath);
const normalizedLanguage =
typeof language === 'string'
? language.toLowerCase().replace(/[^a-z0-9-]+/g, '')
: '';
let highlighted;
try {
highlighted = hljs.highlight(code, { language }).value;
} catch {
highlighted = escapeHtml(code);
}
将每一行放进有序列表,就能获得有语义的行号,无需客户端脚本:
const numbered = highlighted
.split('\n')
.map((line, index) => {
const content = line.trim().length ? line : ' ';
const lineNumber = (meta.start || 1) + index;
const languageAttr = normalizedLanguage
? ` class="language-${normalizedLanguage}"`
: '';
return `<li value="${lineNumber}"><code${languageAttr}>${content}</code></li>`;
})
.join('\n');
借鉴 Emgithub 的样式与剪贴板支持
这些 HTML 最终放进一个仿照 Emgithub 窗口的容器,不过样式现在由项目本地维护。独立的样式表通过 CSS 变量处理明暗主题,一小段 copy.js 脚本则让 “Copy” 按钮真正执行剪贴板操作。
<div class="gh-embed gh-embed--${theme}">
<div class="gh-embed__meta">
<a class="gh-embed__file" href="${meta.web}" target="_blank" rel="noopener noreferrer">
${meta.filePath}
</a>
<div class="gh-embed__actions">
<a class="gh-embed__raw" href="${meta.raw}" target="_blank" rel="noopener noreferrer">view raw</a>
<button class="gh-embed__copy" data-clipboard>Copy</button>
</div>
</div>
<pre class="gh-embed__pre hljs${languageClass}">
<ol class="gh-embed__ol">${numbered}</ol>
</pre>
</div>`;
点击按钮会复制已经渲染的代码行,并显示一秒钟的 “Copied!” 标签,让读者知道复制成功。
使用短代码
全部接好后,嵌入片段只需粘贴 URL,再选择主题修饰参数。短代码默认使用浅色样式,因此样式参数完全可选。
{% github "https://github.com/TheClooneyCollection/11ty-subspace-builder/blob/main/index.njk" %}
{% github "https://github.com/TheClooneyCollection/11ty-subspace-builder/blob/main/index.njk#L1-L11" "dark" %}
由于 HTML 在服务端生成,代码在 RSS、打印以及禁用 JavaScript 的阅读器中依然可见。我也打算在引用代码时使用 GitHub 的 “Copy permalink” 按钮,将 URL 固定到某个提交哈希,避免仓库演进时文章内容跟着变化。
测试与体会
Eleventy 构建是这里主要的测试手段。运行 npm run build,可以确认短代码能获取远程代码、完成高亮,并生成无警告的有效 HTML。之后只要打开本地预览,点击复制按钮,再浏览输出,确认样式与主题一致即可。
各个部分到位后,这个功能显得如此小巧,让我很喜欢。Eleventy 的异步短代码让构建时获取数据很容易接入,Highlight.js 则省去了手工维护语言定义的麻烦。最重要的是,写作流程简单到只需粘贴一个 GitHub 链接,完全符合我最初构思时的期待。
如果你试用了这个功能,或对自动检测网站主题有想法,欢迎告诉我。我很想继续改进 Subspace Builder 的代码展示方式。