Caddy 配合模板引擎原生渲染 Markdown 笔记

本文记录了在不依赖外部前端框架的条件下,如何使用 Caddy Web 服务器内置的模板引擎(Templates Engine)与静态文件服务,将服务器指定目录下的 .md 文件自动渲染为现代化 Markdown 网页的完整配置方案。


1. 架构逻辑与文件结构

整个渲染流程采用“原生路由重写 + 模板渲染”模式:

  1. **访问根路径 /**:显示目录下的文件列表。
  2. 访问 .md 文件:Caddy 捕获请求后内部重写(rewrite)到 view.html,并带上参数 ?f=文件名
  3. **渲染视图 view.html**:通过 Caddy 模板引擎的 includemarkdown 函数解析对应文件并输出。

目录文件布局

/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. 关键踩坑点与运维校验事项

  1. 模板名不能使用 index.html
    若命名为 index.html,Caddy 会默认将其作为首页响应,导致 file_serverbrowse 目录列表失效,点击“返回文件列表”会出现循环刷新。
  2. 重写参数与 OriginalReq 读取
    在 Caddy 配置文件中使用 rewrite @md /view.html?f={path},明确通过 URL Query 参数传递请求路径。模板中使用 .Req.URL.Query.Get "f" 获取路径传给 include 函数,避免 rewrite 之后 .Path 变成 /view.html 导致递归循环抛出 404。
  3. 部署生效命令

    # 校验权限
    sudo chmod -R 755 /srv/markdown
    
    # 重载 Caddy 配置
    sudo caddy reload
    
  4. caddyfile里hide css js,不能隐藏view.html隐藏后,打不开,阿里云的ESA测试,打开边缘加速关闭缓存,能用

标签: none

添加新评论