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
Docute 介绍
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
```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 为
<!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 同步到对象存储,网站刷新一下就能看到了。
时间顺序
后面我把侧边栏按照创建时间排序,使最新的在上面。
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
然后我又加上了 sw.js 。不过 Docute 文档里的离线支持,使用的 Google 服务我打不开。用的还是 Docsify 之前的 sw.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-docsify.js')
}
/* ===========================================================
* 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 的代码,如下:
<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 写一个类似的东西。
欢迎赞赏~
赞赏