21.内容拓展:通过前端JS动态生成并下载.md文件

21.内容拓展:通过前端JS动态生成并下载.md文件

因为静态博客没有后端,我们不能直接提供源文件的下载链接(这会暴露您的整个 source 目录,不安全)。所以,我们将采用一种更聪明的、纯前端的解决方案。

工作原理:

  1. 在Hexo生成文章页面时,我们将该文章的所有元数据(Front-matter)和Markdown正文内容,作为一个数据块(JSON格式)嵌入到页面的HTML中。
  2. 我们在页面上放置一个“下载源文件”的按钮。
  3. 当用户点击按钮时,一段JavaScript代码会被触发。它会读取页面中嵌入的数据,在浏览器中重新拼接出与您原始 .md 文件一模一样的内容。
  4. 最后,JS会创建一个虚拟的下载链接,让用户将这个拼接好的内容保存为 .md 文件。

这个方案既能满足需求,又非常安全,不会暴露您的任何额外信息。


第一步:创建核心JavaScript文件

我们需要修改主题的模板,将每篇文章的数据输出到HTML中。

  1. 新增核心的JS逻辑
  • 文件路径:themes/anzhiyu/source/custom/js/markdown-download.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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
* Hexo AnZhiyu主题 - Markdown源文件下载功能
*
* 功能说明:
* - 为技术博客文章添加Markdown源文件下载功能
* - 包含完整的Front-matter和文章内容
* - 纯前端实现,安全可靠
*
* 作者:Prorise
* 版本:1.0.0
* 兼容:AnZhiyu主题
*/

(function() {
'use strict';

/**
* 将JSON格式的Front-matter数据,转换为YAML字符串
* @param {object} data - 包含title, date等信息的对象
* @returns {string} - 格式化后的YAML字符串
*/
function formatFrontMatterToYAML(data) {
let yamlString = '---\n';
yamlString += `title: ${data.title}\n`;
yamlString += `date: ${data.date}\n`;

// 只有当更新时间与创建时间不同时才添加updated字段
if (data.updated && data.updated !== data.date) {
yamlString += `updated: ${data.updated}\n`;
}

// 添加分类信息
if (data.categories && data.categories.length > 0) {
yamlString += 'categories:\n';
data.categories.forEach(cat => {
yamlString += ` - ${cat}\n`;
});
}

// 添加标签信息
if (data.tags && data.tags.length > 0) {
yamlString += 'tags:\n';
data.tags.forEach(tag => {
yamlString += ` - ${tag}\n`;
});
}

yamlString += '---\n\n';
return yamlString;
}

/**
* 清理文件名中的特殊字符
* @param {string} filename - 原始文件名
* @returns {string} - 清理后的文件名
*/
function sanitizeFilename(filename) {
// 移除或替换Windows/Linux文件系统不支持的字符
return filename.replace(/[<>:"/\\|?*]/g, '_')
.replace(/\s+/g, '_') // 将空格替换为下划线
.replace(/_{2,}/g, '_') // 将多个连续下划线替换为单个
.replace(/^_|_$/g, ''); // 移除开头和结尾的下划线
}

/**
* 创建并触发文件下载
* @param {string} content - 文件内容
* @param {string} filename - 文件名
*/
function downloadFile(content, filename) {
try {
// 创建Blob对象
const blob = new Blob([content], {
type: 'text/markdown;charset=utf-8'
});

// 创建下载链接并触发
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';

document.body.appendChild(a);
a.click();
document.body.removeChild(a);

// 清理URL对象
URL.revokeObjectURL(url);

return true;
} catch (error) {
console.error('文件下载失败:', error);
return false;
}
}

/**
* 验证文章数据的完整性
* @param {object} postData - 文章数据
* @returns {object} - 验证结果 {valid: boolean, message: string}
*/
function validatePostData(postData) {
if (!postData) {
return { valid: false, message: '文章数据不存在' };
}

if (!postData.title) {
return { valid: false, message: '文章标题缺失' };
}

if (!postData.content) {
return { valid: false, message: '文章内容缺失' };
}

if (!postData.date) {
return { valid: false, message: '文章日期缺失' };
}

return { valid: true, message: '数据验证通过' };
}

/**
* 处理下载按钮点击事件
*/
function handleDownloadClick() {
try {
console.log('开始处理Markdown文件下载...');

// 获取文章数据
const postData = window.postData;
console.log('文章数据:', postData);

// 验证数据完整性
const validation = validatePostData(postData);
if (!validation.valid) {
throw new Error(validation.message);
}

// 重新构建Front-matter
const frontMatter = formatFrontMatterToYAML(postData);
console.log('生成的Front-matter:', frontMatter);

// 拼接成完整的Markdown文件内容
const fullContent = frontMatter + postData.content;

// 生成安全的文件名
const safeFilename = sanitizeFilename(postData.title) + '.md';
console.log('生成的文件名:', safeFilename);

// 执行下载
const downloadSuccess = downloadFile(fullContent, safeFilename);

if (downloadSuccess) {
console.log('✅ Markdown文件下载成功:', safeFilename);

// 可选:显示成功提示(如果需要的话)
// 这里可以集成主题的通知系统
if (window.anzhiyu && window.anzhiyu.snackbarShow) {
window.anzhiyu.snackbarShow('📄 Markdown文件下载成功!');
}
} else {
throw new Error('文件下载过程中发生错误');
}

} catch (error) {
console.error('❌ 下载文章源文件失败:', error);
console.error('错误详情:', {
message: error.message,
stack: error.stack,
postData: window.postData
});

// 用户友好的错误提示
alert(`下载失败: ${error.message}\n\n请检查浏览器控制台获取更多技术信息。`);
}
}

/**
* 初始化文章下载按钮的功能
*/
function initArticleDownload() {
console.log('🚀 初始化Markdown下载功能...');

const downloadBtn = document.getElementById('download-md-btn');
const hasPostData = window.postData;

console.log('下载按钮元素:', downloadBtn);
console.log('文章数据可用:', !!hasPostData);

if (downloadBtn && hasPostData) {
// 移除可能存在的旧事件监听器
downloadBtn.removeEventListener('click', handleDownloadClick);

// 添加新的事件监听器
downloadBtn.addEventListener('click', handleDownloadClick);

console.log('✅ Markdown下载功能初始化成功');
} else {
console.warn('⚠️ Markdown下载功能初始化失败:', {
downloadBtn: !!downloadBtn,
postData: !!hasPostData,
currentPage: window.location.pathname
});
}
}

/**
* 页面加载完成后初始化
*/
function onPageReady() {
// 延迟执行,确保页面元素完全加载
setTimeout(initArticleDownload, 100);
}

// 绑定页面加载事件
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', onPageReady);
} else {
onPageReady();
}

// 支持PJAX页面跳转
document.addEventListener('pjax:success', onPageReady);

// 暴露到全局作用域(用于调试)
window.MarkdownDownload = {
init: initArticleDownload,
download: handleDownloadClick,
version: '1.0.0'
};

console.log('📦 Markdown下载模块加载完成 v1.0.0');

})();


第二步:修改文章页面模板

文件路径: themes/anzhiyu/layout/post.pug

操作: 在文件末尾添加以下代码

1
2
3
4
5
6
7
8
9
10
//- 文章源文件下载功能:嵌入文章数据
script.
window.postData = {
title: "#{page.title || ''}",
date: "#{page.date ? page.date.format('YYYY-MM-DD HH:mm:ss') : ''}",
updated: "#{page.updated ? page.updated.format('YYYY-MM-DD HH:mm:ss') : (page.date ? page.date.format('YYYY-MM-DD HH:mm:ss') : '')}",
tags: !{JSON.stringify(page.tags ? page.tags.map(tag => tag.name) : [])},
categories: !{JSON.stringify(page.categories ? page.categories.map(cat => cat.name) : [])},
content: !{JSON.stringify(page._content || '')}
};

第三步:添加下载按钮

文件路径: themes/anzhiyu/layout/includes/rightside.pug

操作1: 找到 when 'comment' 部分,在其后添加:

1
2
3
4
when 'downloadMd'
if is_post()
button#download-md-btn(type="button" title="下载文章源文件")
i.anzhiyufont.anzhiyu-icon-download

操作2: 找到 showArray 定义行,修改为:

1
- const showArray = enable ? show && show.split(',') : ['toc','chat','comment','downloadMd']

第四步:配置文件引用

文件路径:

1
_config.anzhiyu.yml

操作:inject.bottom 部分添加

1
2
bottom:
- '<script src="/custom/js/markdown-download.js"></script>'