跳到主要内容

使用 docusaurus 构建开源电子书

创建电子书

npx create-docusaurus@latest kubernetes-guide classic --typescript

安装依赖

npm install --save docusaurus-plugin-sass sass @giscus/react raw-loader path-browserify flexanalytics/plugin-image-zoom @docusaurus/plugin-ideal-image@latest @docusaurus/plugin-pwa@latest
npm install --save-dev typescript @docusaurus/module-type-aliases @docusaurus/tsconfig @docusaurus/types

自定义样式

src/css/custom.css 重命名为 src/css/custom.scss,覆盖为以下内容:

src/css/custom.scss
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/

/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}

/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

/* 代码高亮行的样式 */
.code-block-highlighted-line {
background-color: rgb(72, 77, 91);
span[class*='codeLineNumber'] {
background-color: rgb(72, 77, 91);
}
}
.code-block-add-line {
background-color: #213227;
span[class*='codeLineNumber'] {
background-color: #213227;
}
}
.code-block-update-line {
background-color: #362d1e;
span[class*='codeLineNumber'] {
background-color: #362d1e;
}
}
.code-block-error-line {
background-color: #ff000020;
span[class*='codeLineNumber'] {
background-color: #ff000020;
}
}

清理不需要的文件

因为是电子书,不需要默认的首页,也不需要博客页面,直接用电子书作为首页。很多默认生成的文件是不需要的,可以删除:

rm -rf src/components/HomepageFeatures
rm -rf src/pages/*
rm -rf blog
rm -rf static/img/*

创建自定义组件

giscus 评论组件

src/components/Comment.tsx
import React from 'react'
import { useThemeConfig, useColorMode } from '@docusaurus/theme-common'
import Giscus, { GiscusProps } from '@giscus/react'
import { useLocation } from '@docusaurus/router';

const defaultConfig: Partial<GiscusProps> = {
id: 'comments',
mapping: 'specific',
reactionsEnabled: '1',
emitMetadata: '0',
inputPosition: 'top',
loading: 'lazy',
strict: '1', // 用根据路径标题自动生成的 sha1 值,精确匹配 github discussion,避免路径重叠(比如父和子路径)时评论加载串了
lang: 'zh-CN',
}

export default function Comment(): JSX.Element {
const themeConfig = useThemeConfig()

// merge default config
const giscus = { ...defaultConfig, ...themeConfig.giscus }

if (!giscus.repo || !giscus.repoId || !giscus.categoryId) {
throw new Error(
'You must provide `repo`, `repoId`, and `categoryId` to `themeConfig.giscus`.',
)
}

const path = useLocation().pathname.replace(/^\/|\/$/g, '');
const firstSlashIndex = path.indexOf('/');
var subPath: string = ""
if (firstSlashIndex !== -1) {
subPath = path.substring(firstSlashIndex + 1)
} else {
subPath = "index"
}

giscus.term = subPath
giscus.theme =
useColorMode().colorMode === 'dark' ? 'transparent_dark' : 'light'

return (
<Giscus {...giscus} />
)
}

增强代码块 FileBlock

src/components/FileBlock.tsx
import React from 'react';
import CodeBlock from '@theme/CodeBlock';
import { useLocation } from '@docusaurus/router';
import * as path from 'path-browserify';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

let extToLang = new Map([
["sh", "bash"],
["yml", "yaml"]
]);

export default function FileBlock({ file, showFileName, ...prop }: { file: string, showFileName?: boolean }) {
// get url path without "/" prefix and suffix
var urlPath = useLocation().pathname.replace(/^\/|\/$/g, '');

// remove locale prefix in urlPath
const { i18n } = useDocusaurusContext()
if (i18n.currentLocale != i18n.defaultLocale) {
urlPath = urlPath.replace(/^[^\/]*\/?/g, '')
}

// find file content according to topPath and file path param
var filepath = ""
if (file.startsWith("@site/")) {
filepath = file.replace(/^@site\//g, '')
} else {
filepath = "codeblock/" + file
}

// load file raw content according to filepath
var content = require('!!raw-loader!@site/' + filepath)?.default
content = content.replace(/\t/g, " "); // replace tab to 2 spaces

// infer language of code block based on filename extension if language is not set
const filename = path.basename(file);
if (!prop.language) {
var language = path.extname(filename).replace(/^\./, '')
const langMappingName = extToLang.get(language)
if (langMappingName) {
language = langMappingName
}
prop.language = language
}

// set title to filename if showFileName is set and title is not set
if (!prop.title && showFileName) {
prop.title = filename
}

return (
<CodeBlock {...prop}>
{content}
</CodeBlock>
);
}

添加文档

在 docs 目录下新建文档:

website # website root directory
├── docs
│ │── intro.md
│ │── hello.md
│ └── tutorial-basics
│ └──create-a-document.md
├── ...

配置侧边栏

电子书的侧边栏(目录)是通过配置文件 sidebars.js 配置的,默认文件路径是项目根目录下的 ./sidebar.js

docusaurus.config.js
export default {
presets: [
[
'classic',
{
docs: {
sidebarPath: './sidebars.js',
},
},
],
],
};

如果希望侧边栏目录根据 docs 目录下的文件自动生成,可以使用类似如下的配置:

sidebars.js
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
const sidebars: SidebarsConfig = {
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
};

export default sidebars;

如果希望自定义侧边栏目录,可以使用类似如下的配置:

import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
const sidebars: SidebarsConfig = {
tutorialSidebar: [
'intro', // 第一个是首页
'hello',
{ // 目录
type: 'category',
label: 'Tutorial',
items: [
'tutorial-basics/create-a-document'
],
},
],
};

export default sidebars;

国际化

当电子书需要支持多语言时,先配置下 i18n,并加上切换语言的下拉框:

export default {
i18n: {
defaultLocale: 'zh',
locales: ['zh', 'en'],
localeConfigs: {
zh: {
label: '中文',
htmlLang: 'zh-CN',
},
en: {
label: 'English',
htmlLang: 'en-US',
},
}
},
themeConfig: {
navbar: {
items: [
// ...
{
type: 'localeDropdown', // 语言选择下拉框
position: 'right', // 放在右上方
},
// ...
],
},
},
// ...
};

然后初始化 i18n 目录:

npm run write-translations -- --locale en

将未翻译的 markdown 文件复制到目标语言目录下:

mkdir -p i18n/en/docusaurus-plugin-content-docs/current
cp -r docs/** i18n/en/docusaurus-plugin-content-docs/current

然后将内容原地翻译成目标语言(可利用 AI 进行翻译),翻译完成后最终构建出来的电子书就支持国际化了,可点击语言选择下拉框来切换语言(参考TKE 实践指南)。

预览不支持多语言切换,需显式指定 locale:

npm run start -- --locale en

支持多版本

TODO

多文档共存

TODO

参考 Kubernetes 排障指南

参考资料