Caddy配合模板引擎原生渲染Markdown
Caddy 配合模板引擎原生渲染 Markdown 笔记
本文记录了在不依赖外部前端框架的条件下,如何使用 Caddy Web 服务器内置的模板引擎(Templates Engine)与静态文件服务,将服务器指定目录下的 .md 文件自动渲染为现代化 Markdown 网页的完整配置方案。
1. 架构逻辑与文件结构
整个渲染流程采用“原生路由重写 + 模板渲染”模式:
- **访问根路径
/**:显示目录下的文件列表。 - 访问
.md文件:Caddy 捕获请求后内部重写(rewrite)到view.html,并带上参数?f=文件名。 - **渲染视图
view.html**:通过 Caddy 模板引擎的include与markdown函数解析对应文件并输出。
目录文件布局
/srv/markdown/
├── view.html # 核心 HTML 视图模板
├── a1.md # Markdown 源文件
├── css/
│ ├── github-markdown.min.css
│ └── github-highlight.min.css
└── js/
└── highlight.min.js
三个渲染文件下载
curl -o css/github-highlight.min.css https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-highlight.min.css
curl -o css/github-markdown.min.css https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown.min.css
curl -o js/highlight.min.js https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js
2. Caddyfile 配置文件
以下为完整且经过验证的 Caddyfile 配置,包含通用代码片段、客户端 IP 解析、重写逻辑与安全防范机制。
{
log {
output stderr
level ERROR
}
servers {
# 信任代理网段,保证通过反向代理/CDN 访问时能正确识别客户端 IP
trusted_proxies static private_ranges
client_ip_headers X-Forwarded-For
}
}
# 基础响应头与压缩配置片段
(zbb) {
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
}
encode zstd gzip
log {
format console
level ERROR
}
}
zjianli.zhaopeng.site {
root * /srv/markdown
import zbb
templates
# 当用户点击 a1.md 时,重写到 /view.html 并带上 ?f=a1.md 参数
@md path *.md
rewrite @md /view.html?f={path}
file_server {
browse
hide css js
}
}
3. 模板文件 view.html
视图文件集成了响应式排版、深浅色模式自动适配、表格边框优化、代码高亮、一键复制及阅读进度条功能。请将以下完整代码写入 /srv/markdown/view.html。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Markdown 预览</title>
<!-- 引入 GitHub Markdown 基础样式与 Highlight.js 高亮 -->
<link rel="stylesheet" href="/css/github-markdown.min.css">
<link rel="stylesheet" href="/css/github-highlight.min.css">
<style>
/* 调色盘与基础定义 */
:root {
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
--font-code: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
/* 浅色模式配色 */
--bg-main: #f8fafc;
--card-bg: #ffffff;
--border-color: #e2e8f0;
--text-main: #1e293b;
--text-muted: #64748b;
--h1-color: #0f172a;
--h2-color: #1e3a8a;
--h3-color: #2563eb;
--link-color: #2563eb;
--link-hover: #1d4ed8;
--quote-bg: #f1f5f9;
--quote-border: #3b82f6;
--quote-text: #334155;
--inline-code-bg: #f1f5f9;
--inline-code-color: #0f766e;
--btn-bg: #ffffff;
--btn-hover: #f8fafc;
--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.01);
}
/* 自动跟随系统深色模式 */
@media (prefers-color-scheme: dark) {
:root {
--bg-main: #0f172a;
--card-bg: #1e293b;
--border-color: #334155;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--h1-color: #f8fafc;
--h2-color: #60a5fa;
--h3-color: #93c5fd;
--link-color: #60a5fa;
--link-hover: #93c5fd;
--quote-bg: #0f172a;
--quote-border: #3b82f6;
--quote-text: #cbd5e1;
--inline-code-bg: #0f172a;
--inline-code-color: #2dd4bf;
--btn-bg: #1e293b;
--btn-hover: #334155;
--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
}
}
body {
background-color: var(--bg-main);
margin: 0;
padding: 24px 16px;
font-family: var(--font-main);
color: var(--text-main);
color-scheme: light dark;
transition: background-color 0.2s ease, color 0.2s ease;
}
/* 阅读进度条 */
#progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, #3b82f6, #2dd4bf);
width: 0%;
z-index: 9999;
transition: width 0.1s linear;
}
.container {
max-width: 960px;
margin: 0 auto;
}
/* 顶栏与返回按钮 */
.header-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background-color: var(--btn-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--link-color);
text-decoration: none;
font-size: 14px;
font-weight: 600;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.back-btn:hover {
background-color: var(--btn-hover);
color: var(--link-hover);
text-decoration: none;
transform: translateY(-1px);
}
.file-tag {
font-size: 13px;
color: var(--text-muted);
background: var(--btn-bg);
border: 1px solid var(--border-color);
padding: 4px 12px;
border-radius: 20px;
font-family: var(--font-code);
}
/* Markdown 核心容器与样式渲染 */
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 960px;
margin: 0 auto;
padding: 48px;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: var(--shadow);
line-height: 1.8;
letter-spacing: 0.2px;
color: var(--text-main);
font-family: var(--font-main);
}
/* 标题层级 */
.markdown-body h1 {
color: var(--h1-color) !important;
font-size: 2em !important;
font-weight: 800 !important;
border-bottom: 2px solid var(--border-color) !important;
padding-bottom: 12px !important;
margin-top: 24px !important;
margin-bottom: 20px !important;
}
.markdown-body h2 {
color: var(--h2-color) !important;
font-size: 1.5em !important;
font-weight: 700 !important;
border-bottom: 1px solid var(--border-color) !important;
padding-bottom: 8px !important;
margin-top: 32px !important;
margin-bottom: 16px !important;
border-left: 4px solid var(--quote-border);
padding-left: 10px !important;
}
.markdown-body h3 {
color: var(--h3-color) !important;
font-size: 1.25em !important;
font-weight: 600 !important;
margin-top: 24px !important;
margin-bottom: 12px !important;
}
/* 超链接 */
.markdown-body a {
color: var(--link-color) !important;
text-decoration: none !important;
font-weight: 500;
border-bottom: 1px dashed var(--link-color);
transition: all 0.2s ease;
}
.markdown-body a:hover {
color: var(--link-hover) !important;
border-bottom: 1px solid var(--link-hover);
}
/* 加粗 */
.markdown-body strong {
color: var(--h1-color) !important;
font-weight: 700 !important;
}
/* 行内代码 */
.markdown-body code:not(pre code) {
background-color: var(--inline-code-bg) !important;
color: var(--inline-code-color) !important;
font-family: var(--font-code) !important;
padding: 3px 6px !important;
border-radius: 5px !important;
border: 1px solid var(--border-color) !important;
font-size: 0.88em !important;
}
/* 引用块 */
.markdown-body blockquote {
background-color: var(--quote-bg) !important;
border-left: 4px solid var(--quote-border) !important;
color: var(--quote-text) !important;
padding: 14px 20px !important;
margin: 20px 0 !important;
border-radius: 0 8px 8px 0 !important;
font-style: normal !important;
}
/* 表格样式增强 */
.markdown-body table {
display: table !important;
width: 100% !important;
border-collapse: separate !important;
border-spacing: 0 !important;
margin: 24px 0 !important;
border: 1px solid var(--border-color) !important;
border-radius: 8px !important;
overflow: hidden !important;
}
.markdown-body table th,
.markdown-body table td {
padding: 12px 16px !important;
border-bottom: 1px solid var(--border-color) !important;
border-right: 1px solid var(--border-color) !important;
}
.markdown-body table tr:last-child td {
border-bottom: none !important;
}
.markdown-body table th:last-child,
.markdown-body table td:last-child {
border-right: none !important;
}
.markdown-body table th {
font-weight: 600 !important;
background-color: var(--quote-bg) !important;
color: var(--h1-color) !important;
}
.markdown-body table tr:hover td {
background-color: rgba(59, 130, 246, 0.04);
}
/* 代码块复制按钮组件 */
.code-wrapper {
position: relative;
}
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
padding: 4px 10px;
font-size: 12px;
color: var(--text-muted);
background: var(--btn-bg);
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
opacity: 0.7;
transition: all 0.2s ease;
}
.code-wrapper:hover .copy-btn {
opacity: 1;
}
.copy-btn:hover {
background: var(--btn-hover);
color: var(--link-color);
}
@media (max-width: 767px) {
body { padding: 12px 8px; }
.markdown-body { padding: 24px 16px; }
}
</style>
</head>
<body>
<div id="progress-bar"></div>
<div class="container">
<div class="header-bar">
<a href="/" class="back-btn">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M7.78 12.53a.75.75 0 0 1-1.06 0L2.22 8.03a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 1.06L4.31 7h9.44a.75.75 0 0 1 0 1.5H4.31l3.47 3.47a.75.75 0 0 1 0 1.06Z"/></svg>
返回文件列表
</a>
{{ $file := .Req.URL.Query.Get "f" }}
{{ if $file }}
<span class="file-tag">FILE: {{ $file }}</span>
{{ end }}
</div>
<article class="markdown-body">
{{ if $file }}
{{ include $file | markdown }}
{{ else }}
<p style="color: var(--text-muted); text-align: center; margin: 40px 0;">请从文件列表中选择要查看的 Markdown 文件。</p>
{{ end }}
</article>
</div>
<script src="/js/highlight.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// 语法高亮初始化
if (window.hljs) hljs.highlightAll();
// 动态添加一键复制按钮
document.querySelectorAll('.markdown-body pre').forEach((pre) => {
const wrapper = document.createElement('div');
wrapper.className = 'code-wrapper';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const button = document.createElement('button');
button.className = 'copy-btn';
button.innerText = '复制';
button.addEventListener('click', async () => {
const code = pre.querySelector('code') ? pre.querySelector('code').innerText : pre.innerText;
try {
await navigator.clipboard.writeText(code);
button.innerText = '已复制';
button.style.color = '#10b981';
setTimeout(() => {
button.innerText = '复制';
button.style.color = '';
}, 2000);
} catch (err) {
button.innerText = '失败';
}
});
wrapper.appendChild(button);
});
// 顶部阅读进度条控制
window.addEventListener('scroll', () => {
const winScroll = document.documentElement.scrollTop || document.body.scrollTop;
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
document.getElementById('progress-bar').style.width = (scrolled || 0) + '%';
});
});
</script>
</body>
</html>
4. 关键踩坑点与运维校验事项
- 模板名不能使用 index.html:
若命名为index.html,Caddy 会默认将其作为首页响应,导致file_server的browse目录列表失效,点击“返回文件列表”会出现循环刷新。 - 重写参数与 OriginalReq 读取:
在 Caddy 配置文件中使用rewrite @md /view.html?f={path},明确通过 URL Query 参数传递请求路径。模板中使用.Req.URL.Query.Get "f"获取路径传给include函数,避免rewrite之后.Path变成/view.html导致递归循环抛出 404。 部署生效命令:
# 校验权限 sudo chmod -R 755 /srv/markdown # 重载 Caddy 配置 sudo caddy reload- caddyfile里hide css js,不能隐藏view.html隐藏后,打不开,阿里云的ESA测试,打开边缘加速关闭缓存,能用