Obsidian 公开库从 Docsify 改为 Docute

最近把 Obsidian 公开库从 Docsify 折腾为了 Docute。

大概是 2022 年 9 月,我使用 Obsidian Remotely Save + 对象存储静态网站托管 + Docsify 搭建了一个公开库。也放了一些静态网站,后来一些文件也被用于博客网站的数据源。

这其中,Docsify 只需要一个 index.html 和一些 markdown 文件就可以部署为网站。需要做的就是一些索引工作。这之前,我使用的是 Zoottelkeeper 生成索引链接。这其实并不能完全符合我的要求,但是也不想自己折腾插件,于是也就这样了。

2024-02-21 又看到了 Docute ,顺手把 Docsify 改为 Docute 。效果 https://note.ftls.xyz/#/

Docute https://docute.egoist.dev/zh/

官网的话是

Docute 本质上就是一个 JavaScript 文件,它可以获取 Markdown 文件并将它们呈现为单页面应用。
它完全由运行时驱动,因此并不涉及服务端组件,这就意味着没有构建过程。你只需创建一个 HTML 文件和一堆 Markdown 文档,你的网站就差不多完成了!

Docsify 和 Docute 几乎相同,但具有不同的 UI 和不同的使用方式。
Docute(60kB)比 Docisfy(20kB)大 3 倍,因为我们使用了 Vue,Vue Router 和 Vuex,而 Docsify 使用的是 vanilla JavaScript。

实际上这个大小数据已经过时了,Docute 现在 200kb+ 。 如果自己写的自己需要的功能,大概会小一些。

虽然 200kb 很大,但我今天才发现我用的 Docsify icon 就 60kb+

Docute 功能上很多都是打包好的,js 比 Docisfy 大,配置项却比 Docisfy 少,倒是也够用了。使用上最大的区别是侧边栏生成方式不同,Docsify 使用的是 .md 文件渲染。并且似乎在内部修改了渲染 markdown list 的方法,在 index.html 重新定义渲染方法会导致侧边栏没有 css 效果。 不过问题不大,我也不用侧边栏,只是一个文档站点。正好 Docute 没有这个问题。

不过使用 js 对象, Zoottelkeeper 的插件生成的文件是满足不了 Docute 的需求了。于是我选择了 RunJS 插件生成文件,来为 Docute 提供侧边栏。

在 Obsidian 中一个 .md 文件中写入下面代码,根目录创建 sidebar.md。然后带点侧边 JS 图标,运行 List 就行了。程序会读取 markdown 文件,并把最后对象写入文件,给 Docute 使用。

sidebar.md 虽然是 markdown 文件,但是确实是 JSON 内容,写成 markdown 是为了在 Obsidian 就可以观察到变化。

RunJS 是调用的 Obsidian 的 API ,Obsidian 文件列出,创建,修改 API 参考 https://docs.obsidian.md/Reference/TypeScript+API/Vault

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
```js RunJS="List"
new Notice("生成索引文件");
// import * as obsidian from 'obsidian';
const runJS = this;
let pagesFile = [];
let journalsFiles = [];
let ZKFiles = [];
this.app.vault.getMarkdownFiles().map((file) => {
    if(file.path.startsWith("pages/")) {
        pagesFile.push({
            title : file.path.replace("pages/","").replace(".md",""),
            link : "/"+file.path.slice(0,file.path.length-3),
        });
    }
    if(file.path.startsWith("journals/")) {
        journalsFiles.push({
            title : file.path.replace("journals/","").replace(".md",""),
            link : "/"+file.path.slice(0,file.path.length-3),
        });
    }
    if(file.path.startsWith("ZK/")) {
        ZKFiles.push({
            title : file.path.replace("ZK/","").replace(".md",""),
            link : "/"+file.path.slice(0,file.path.length-3),
        });
    }
    // new Notice(file.path);
    // result += file.path + "\n";
})
let obj = [
    {
        title: "Pages",
        children: pagesFile
    },
    {
        title: "Journals",
        children: journalsFiles
    },
    {
        title: "ZK",
        children: ZKFiles
    }
]
const xxxx = this.app.vault.getAbstractFileByPath("sidebar.md");
this.app.vault.modify(xxxx, JSON.stringify(obj)).then(res => {
    new Notice("Finish!");
})

```

Obsidian 库根目录下的 index.html 为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>笔记</title>
    <link rel="stylesheet" href="https://cdn.ftls.xyz/js/docute@4.23.3/docute.css">
</head>

<body>
    <div id="docute"></div>
    <script src="https://cdn.ftls.xyz/js/docute@4.23.3/docute.js"></script>
    <script>
        const mdProcess = {
            name: 'mdProcess',
            extend(api) {
                api.processMarkdown(text => {
                    return text
                        .replace(/\[\[(.*?)\|(.*?)\]\]/g, `[$2]($1)`) // 匹配[[path|name]]
                        .replace(/\[\[(.*?)\]\]/g, '[$1]($1)') // 匹配 [[path]]
                        .replace(/%% .*? %%/g, ""); // 匹配插件生成的额外文字
                })
            }
        }

        const markedRender = {
            name: 'markedRender',
            extend(api) {
                api.extendMarkedRenderer(marked => {
                    marked.options.breaks = true;
                    marked.options.smartypants = true;
                })
            }
        }

        fetch("/sidebar.md").then((res) => res.json()).then((obj) => {
            new Docute({
                title: "Note",
                target: '#docute',
                sidebar: obj,
                darkThemeToggler: true,
                plugins: [
                    mdProcess,
                    markedRender
                ]
            })
        });
    </script>
</body>

</html>

这样每次创建 .md 文件,RunJS 运行下 List,Remotely Save 同步到对象存储,网站刷新一下就能看到了。

后面我把侧边栏按照创建时间排序,使最新的在上面。

1
2
3
4
5
6
let files = this.app.vault.getMarkdownFiles();
files.sort((a,b) => {
     //new Notice(a.stat.ctime - b.stat.ctime);
    return b.stat.ctime - a.stat.ctime;
});
files.map((file) => {})

然后我又加上了 sw.js 。不过 Docute 文档里的离线支持,使用的 Google 服务我打不开。用的还是 Docsify 之前的 sw.js

1
2
3
        if ('serviceWorker' in navigator) {
          navigator.serviceWorker.register('/sw-docsify.js')
        }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/* ===========================================================
 * docsify sw.js
 * ===========================================================
 * Copyright 2016 @huxpro
 * Licensed under Apache 2.0
 * Register service worker.
 * ========================================================== */

const RUNTIME = 'docsify'
const HOSTNAME_WHITELIST = [
  self.location.hostname,
  'fonts.gstatic.com',
  'fonts.googleapis.com',
  'cdn.jsdelivr.net'
]

// The Util Function to hack URLs of intercepted requests
const getFixedUrl = (req) => {
  var now = Date.now()
  var url = new URL(req.url)

  // 1. fixed http URL
  // Just keep syncing with location.protocol
  // fetch(httpURL) belongs to active mixed content.
  // And fetch(httpRequest) is not supported yet.
  url.protocol = self.location.protocol

  // 2. add query for caching-busting.
  // Github Pages served with Cache-Control: max-age=600
  // max-age on mutable content is error-prone, with SW life of bugs can even extend.
  // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
  // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
  if (url.hostname === self.location.hostname) {
    url.search += (url.search ? '&' : '?') + 'cache-bust=' + now
  }
  return url.href
}

/**
 *  @Lifecycle Activate
 *  New one activated when old isnt being used.
 *
 *  waitUntil(): activating ====> activated
 */
self.addEventListener('activate', event => {
  event.waitUntil(self.clients.claim())
})

/**
 *  @Functional Fetch
 *  All network requests are being intercepted here.
 *
 *  void respondWith(Promise<Response> r)
 */
self.addEventListener('fetch', event => {
  // Skip some of cross-origin requests, like those for Google Analytics.
  if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {
    // Stale-while-revalidate
    // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
    // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
    const cached = caches.match(event.request)
    const fixedUrl = getFixedUrl(event.request)
    const fetched = fetch(fixedUrl, { cache: 'no-store' })
    const fetchedCopy = fetched.then(resp => resp.clone())

    // Call respondWith() with whatever we get first.
    // If the fetch fails (e.g disconnected), wait for the cache.
    // If there’s nothing in cache, wait for the fetch.
    // If neither yields a response, return offline pages.
    event.respondWith(
      Promise.race([fetched.catch(_ => cached), cached])
        .then(resp => resp || fetched)
        .catch(_ => { /* eat any errors */ })
    )

    // Update the cache with the version we fetched (only for ok status)
    event.waitUntil(
      Promise.all([fetchedCopy, caches.open(RUNTIME)])
        .then(([response, cache]) => response.ok && cache.put(event.request, response))
        .catch(_ => { /* eat any errors */ })
    )
  }
})

大概就是这些。另外,我这 BearBlog 学到一种用 Emoji 表情作为 icon 的代码,如下:

1
<link rel="shortcut icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%3E%3Ctext%20y='.9em'%20font-size='90'%3E♾️%3C/text%3E%3C/svg%3E">

放在 HTML <head></<head> 里面就行了。

Docute 看起来还不错,UI 挺耐看的。不过可能哪天我又换成 Docsify 了,或者其他什么。

Docute 侧边栏是不支持多级目录的,也就是 children 层数有限,就想起了 Naive UI 树 看着不错了。我之前就觉得 Naive UI 确实很好看来着,可能哪天用 Naive UI 写一个类似的东西。