格盘忘记备份笔记了

如题,没备份,那咋办嘛,数据恢复工具也没扫出来,只能说是格的非常干净了
想到有个静态存储库,想着用工具恢复看看

写了个恢复的小工具

网上确实有个工具,但是是七年前写的,太辣了,根本用不了,就拷打cursor写了个,恢复的效果还不错

恢复后的文章基本保留了我原有的Front-matte

就是时间啥的可能有点问题,毕竟主要是根据静态存储库恢复的

嗯稍微研究下了配置项

之前为了文章排序的问题,是自己改了主题渲染模板以及写了个js脚本辅助,date: 2025-04-01 16:00:00 updated: 2025-08-14 categories: 学习 tags: - 日常随记 --- # updated判断 在hexo目录下创建脚本文件`scripts/custom-i 这记录了,但刚恢复,不想那么麻烦,研究了下配置项,发现。。。。。。本来就有的,设置为updated就行


md还是要写插件,要不然就会出现updated的值相同,但是date的值在更早的文章会排在上面,还是要用到date: 2025-04-01 16:00:00 updated: 2025-08-14 categories: 学习 tags: - 日常随记 --- # updated判断 在hexo目录下创建脚本文件`scripts/custom-i 这里写的插件,但是主题渲染模板不需要修改

解决了相对链接问题

恢复的文章中有很多相对路径的内部链接,比如 [这](../笔记/笔记4---修改了主题模板,增加了updated判断),但是在 Hexo 中,文章的 URL 是根据 permalink 配置生成的(如 :year/:month/:day/:title/),而不是按照文件夹结构,所以这种相对路径链接会失效。

问题分析

  • Hexo 的 URL 结构:配置 permalink: :year/:month/:day/:title/ 会将文章转换为类似 /2025/04/01/笔记4---修改了主题模板,增加了updated判断/ 的URL
  • 相对路径问题../笔记/笔记4---修改了主题模板,增加了updated判断 这种文件系统路径在网页中无效
  • 错误的拼接:浏览器会将当前文章的URL与相对路径拼接,导致错误的路径

解决方案:自动转换插件

写了一个Hexo 插件来自动将相对路径链接转换为正确的 {% post_link %} 标签,这样在本地写作时依然可以使用自然的相对路径,插件会在构建时自动处理。

创建文件 scripts/relative-link-converter.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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
* Hexo 相对链接自动转换插件
* 将 [文本](../folder/filename) 形式的相对链接自动转换为 {% post_link %} 标签
* 跳过代码块和行内代码
*/

'use strict';

const fs = require('fs');
const path = require('path');

let allPosts = null; // 缓存所有文章标题

// 扫描所有文章文件,提取标题
function scanAllPosts() {
if (allPosts) return allPosts; // 如果已经缓存就直接返回

allPosts = new Map();
const postsDir = path.join(hexo.source_dir, '_posts');

function scanDir(dir) {
try {
const items = fs.readdirSync(dir);
items.forEach(item => {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) {
scanDir(fullPath); // 递归扫描子目录
} else if (item.endsWith('.md')) {
// 读取文件内容提取标题
try {
const content = fs.readFileSync(fullPath, 'utf8');
const titleMatch = content.match(/^title:\s*["']?([^"'\\n]+)["']?/m);
if (titleMatch) {
const title = titleMatch[1].trim();
const filename = path.basename(item, '.md');
allPosts.set(title, title);
allPosts.set(filename, title); // 也映射文件名到标题
}
} catch (e) {
console.warn(`[相对链接扫描] 读取文件失败: ${fullPath}`, e.message);
}
}
});
} catch (e) {
console.warn(`[相对链接扫描] 扫描目录失败: ${dir}`, e.message);
}
}

scanDir(postsDir);
console.log(`[相对链接扫描] 扫描完成,找到 ${allPosts.size / 2} 篇文章`);
console.log(`[相对链接扫描] 可用的文章标题:`, Array.from(new Set(Array.from(allPosts.values()))));

return allPosts;
}

function protectCodeBlocks(content) {
// 保护多行代码块
const codeBlocks = [];
content = content.replace(/```[\s\S]*?```/g, match => {
codeBlocks.push(match);
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
});
// 保护行内代码
const inlineCodes = [];
content = content.replace(/`[^`]+`/g, match => {
inlineCodes.push(match);
return `__INLINE_CODE_${inlineCodes.length - 1}__`;
});
return { content, codeBlocks, inlineCodes };
}

function restoreCodeBlocks(content, codeBlocks, inlineCodes) {
content = content.replace(/__CODE_BLOCK_(\d+)__/g, (m, i) => codeBlocks[Number(i)]);
content = content.replace(/__INLINE_CODE_(\d+)__/g, (m, i) => inlineCodes[Number(i)]);
return content;
}

hexo.extend.filter.register('before_post_render', function(data) {
// 只处理 markdown 文件
if (!data.source || !data.source.endsWith('.md')) return data;

// 保护代码块和行内代码
const { content: protectedContent, codeBlocks, inlineCodes } = protectCodeBlocks(data.content);

// 匹配相对路径链接的正则表达式
const relativeLinkRegex = /\[([^\]]+)\]\(\.\.\/[^)]+\/([^/)]+)\)/g;

const matches = protectedContent.match(relativeLinkRegex);
if (!matches) {
// 没有相对链接,直接还原代码块返回
data.content = restoreCodeBlocks(protectedContent, codeBlocks, inlineCodes);
return data;
}

// 扫描所有文章
const titleMap = scanAllPosts();

console.log(`[相对链接检测] 在 ${data.source} 中找到 ${matches.length} 个相对链接:`, matches);

// 替换相对链接
let replaced = protectedContent.replace(relativeLinkRegex, (match, linkText, filename) => {
// 从文件名中移除可能的扩展名
const cleanFilename = filename.replace(/\.(md|html)$/, '');
console.log(`[相对链接处理] 处理链接: ${match}, 提取的文件名: "${cleanFilename}"`);

// 尝试通过文件名找到对应的文章
let targetPost = null;

// 方法1: 直接匹配标题或文件名
if (titleMap.has(cleanFilename)) {
targetPost = titleMap.get(cleanFilename);
console.log(`[相对链接处理] 精确匹配成功: "${cleanFilename}" -> "${targetPost}"`);
}

// 方法2: 如果没找到,尝试模糊匹配(移除特殊字符)
if (!targetPost) {
const normalizedFilename = cleanFilename
.replace(/[^\w\u4e00-\u9fff]/g, '') // 只保留字母、数字和中文
.toLowerCase();
console.log(`[相对链接处理] 尝试模糊匹配,标准化文件名: "${normalizedFilename}"`);

for (const [key, title] of titleMap) {
const normalizedKey = key
.replace(/[^\w\u4e00-\u9fff]/g, '')
.toLowerCase();

if (normalizedKey === normalizedFilename) {
targetPost = title;
console.log(`[相对链接处理] 模糊匹配成功: "${normalizedFilename}" -> "${title}"`);
break;
}
}
}

// 如果找到了对应的文章,转换为 post_link 标签
if (targetPost) {
console.log(`[相对链接转换] ${match} -> {% post_link ${targetPost} ${linkText} %}`);
return `{% post_link ${targetPost} ${linkText} %}`;
} else {
// 如果找不到对应文章,保持原样并输出警告
console.warn(`[相对链接转换] 警告: 无法找到对应文章 "${cleanFilename}"`);
return match;
}
});

// 还原代码块和行内代码
data.content = restoreCodeBlocks(replaced, codeBlocks, inlineCodes);
return data;
});

console.log('[插件加载] 相对链接自动转换插件已激活');

这样,文章中用类似 [这](../笔记/xxx) 或放在多行代码块里的内容都不会被自动替换,展示和写作都更自然了。

嗯没啥好说,只能说,多留备份吧