Markdown论坛阅读器

Markdown论坛阅读器 - 轻量级知识管理解决方案

项目简介

Markdown论坛阅读器是一个优雅、高效的单文件PHP应用程序,专门用于将Markdown格式的技术讨论、博客文章或论坛内容以美观的网页形式呈现。它完美结合了简洁的部署方式和强大的功能特性,是个人知识管理和小型团队内容分享的理想选择。

核心特性

🚀 一键部署

  • 单文件实现,无需复杂配置
  • 零数据库依赖,直接读取Markdown文件
  • 支持主流PHP环境(PHP 5.6+)

📝 完美Markdown支持

  • 完整支持CommonMark标准
  • 代码语法高亮(集成highlight.js)
  • 表格、列表、引用等丰富格式
  • 图片自适应和标题说明

🧮 数学公式渲染

  • 集成MathJax引擎
  • 支持LaTeX数学公式语法
  • 行内公式:$E = mc^2$
  • 块级公式:$$\sum_{i=1}^n i = \frac{n(n+1)}{2}$$

🔍 智能全文搜索

  • 实时内容检索
  • 标题和内容权重匹配
  • 搜索结果高亮显示
  • 相关度智能排序

📅 时间线归档

  • 按年份、月份自动分组
  • 清晰的时间线浏览体验
  • 快速导航跳转

🎨 响应式设计

  • 移动端友好适配
  • 现代化UI界面
  • 可自定义主题色彩
  • 优雅的动画交互

快速开始

环境要求

  • PHP 5.6 或更高版本
  • Web服务器(Apache/Nginx)或PHP内置服务器
  • 支持Markdown文件存储的目录权限

安装步骤

  1. 下载必要文件

下载主程序

将下面代码保存为index.php,上传到服务器根目录

下载Parsedown解析器

去https://parsedown.org/网站下载 Parsedown.php,上传到服务器根目录

创建内容目录

mkdir bbs

  1. 配置基础信息index.php中修改配置常量:

    define('MD_DIR', __DIR__ . '/bbs');      // 内容目录
    define('SITE_TITLE', '我的技术论坛');     // 网站标题
    define('THEME_COLOR', '#2563eb');        // 主题色彩
  2. 添加内容文件bbs目录中创建Markdown文件:

    ---
    title: 我的第一篇文章
    date: 2024-01-15
    author: 作者姓名
    tags: PHP,Markdown,教程
    ---
    ## 文章内容
    这是我的第一篇技术文章,支持**粗体**、*斜体*等格式。
  3. 启动服务

    
    # 使用PHP内置服务器
    php -S localhost:8000

或配置到Apache/Nginx


## 内容管理指南

### 文件命名规范
- 使用有意义的英文或拼音命名
- 扩展名必须为`.md`
- 示例:`php-tutorial.md`、`mysql-optimization.md`

### 元数据格式
每篇文章可选的YAML front matter:
```yaml
---
title: 文章标题
date: 2024-01-15          # 发布日期
author: 作者名称           # 文章作者
tags: 标签1,标签2,标签3    # 文章标签(逗号分隔)
---

数学公式使用

行内公式$\nabla \cdot \mathbf{E} = \frac{\rho}{\epsilon_0}$

块级公式

$$
f(x) = \int_{-\infty}^{\infty} \hat f(\xi) e^{2 \pi i \xi x} d\xi
$$

高级功能

搜索优化

  • 搜索词自动高亮
  • 标题匹配权重更高
  • 智能摘要生成

自定义样式

通过修改CSS变量轻松定制:

:root {
    --primary: #your-color;       /* 主色调 */
    --secondary: #64748b;         /* 次要色 */
    --accent: #f1f5f9;            /* 强调色 */
}

性能特性

  • 文件缓存机制
  • 按需内容加载
  • 最小化资源请求

使用场景

🏢 技术团队知识库

  • 代码规范文档
  • 技术方案讨论
  • 项目经验分享

🎓 教育机构

  • 课程讲义发布
  • 学术论文展示
  • 数学公式演示

👨‍💻 个人博客

  • 技术博客管理
  • 学习笔记整理
  • 项目文档维护

🔬 科研团队

  • 研究论文分享
  • 实验数据说明
  • 学术讨论记录

优势对比

特性 传统论坛 本阅读器
部署复杂度 极低
内容格式 有限 丰富Markdown
数学公式 不支持 完美支持
搜索功能 基础 智能全文搜索
维护成本 几乎为零
数据安全 数据库依赖 纯文件存储

技术亮点

  1. 架构优雅:MVC模式的单文件实现
  2. 扩展性强:模块化设计便于功能扩展
  3. 安全可靠:内容安全过滤和验证
  4. 性能优异:轻量级代码,快速响应
  5. 标准兼容:遵循Web标准和Markdown规范

结语

Markdown论坛阅读器重新定义了轻量级内容管理的标准。无论是个人技术博客、团队知识分享,还是学术内容展示,它都能提供专业级的阅读体验和便捷的内容管理。零配置部署、全功能覆盖、优雅体验——让您专注于内容创作,而非技术维护。

立即开始使用,打造属于您的专业知识分享平台!

代码分享:

<?php
/**
 * Markdown论坛阅读器 - 单文件实现
 * 支持公式渲染、全文搜索、按年月分组
 */

// 配置设置
define('MD_DIR', __DIR__ . '/bbs');  // Markdown文件目录
define('SITE_TITLE', 'Markdown论坛阅读器');
define('THEME_COLOR', '#2563eb');
date_default_timezone_set('Asia/Shanghai');

// 引入Parsedown Markdown解析器
require_once 'Parsedown.php';

// 获取请求参数
$page = $_GET['page'] ?? 'home';
$id = $_GET['id'] ?? '';
$query = $_GET['q'] ?? '';
$year = $_GET['year'] ?? '';
$month = $_GET['month'] ?? '';

// 主应用逻辑
$app = new MarkdownReader();
$app->handleRequest($page, $id, $query, $year, $month);

class MarkdownReader {
    private $articles = [];
    private $groupedArticles = [];
    private $parsedown;

    public function __construct() {
        // 初始化Parsedown
        $this->parsedown = new Parsedown();
        $this->parsedown->setSafeMode(false); // 允许HTML,因为我们信任自己的内容
        $this->parsedown->setMarkupEscaped(false); // 不转义标记

        $this->loadArticles();
        $this->groupArticlesByYearMonth();
    }

    public function handleRequest($page, $id, $query, $year, $month) {
        // 路由处理
        if (!empty($query)) {
            $this->renderSearchResults($query);
        } elseif (!empty($id) && isset($this->articles[$id])) {
            $this->renderArticle($id);
        } elseif (!empty($year) && !empty($month)) {
            $this->renderArchive($year, $month);
        } elseif (!empty($year)) {
            $this->renderYearArchive($year);
        } else {
            $this->renderHomepage();
        }
    }

    private function loadArticles() {
        $files = glob(MD_DIR . '/*.md');

        foreach ($files as $file) {
            $id = basename($file, '.md');
            $content = file_get_contents($file);
            $metadata = $this->extractMetadata($content);
            $content = $this->removeMetadata($content);

            $this->articles[$id] = [
                'id' => $id,
                'title' => $metadata['title'] ?? $id,
                'content' => $content,
                'date' => $metadata['date'] ?? date('Y-m-d', filemtime($file)),
                'author' => $metadata['author'] ?? '未知作者',
                'tags' => isset($metadata['tags']) ? explode(',', $metadata['tags']) : []
            ];
        }

        // 按日期排序
        uasort($this->articles, function($a, $b) {
            return strtotime($b['date']) - strtotime($a['date']);
        });
    }

    private function groupArticlesByYearMonth() {
        foreach ($this->articles as $id => $article) {
            $timestamp = strtotime($article['date']);
            $year = date('Y', $timestamp);
            $month = date('m', $timestamp);
            $monthName = date('F Y', $timestamp);

            if (!isset($this->groupedArticles[$year])) {
                $this->groupedArticles[$year] = [
                    'year' => $year,
                    'count' => 0,
                    'months' => []
                ];
            }

            if (!isset($this->groupedArticles[$year]['months'][$month])) {
                $this->groupedArticles[$year]['months'][$month] = [
                    'month' => $month,
                    'month_name' => $monthName,
                    'count' => 0,
                    'articles' => []
                ];
            }

            $this->groupedArticles[$year]['months'][$month]['articles'][$id] = $article;
            $this->groupedArticles[$year]['months'][$month]['count']++;
            $this->groupedArticles[$year]['count']++;
        }
    }

    private function extractMetadata(&$content) {
        $metadata = [];
        $lines = explode("\n", $content);

        if (count($lines) > 0 && trim($lines[0]) === '---') {
            array_shift($lines);
            while (count($lines) > 0 && trim($lines[0]) !== '---') {
                $line = array_shift($lines);
                if (preg_match('/^(\w+):\s*(.+)$/', $line, $matches)) {
                    $metadata[strtolower($matches[1])] = trim($matches[2]);
                }
            }
            array_shift($lines);
            $content = implode("\n", $lines);
        }

        return $metadata;
    }

    private function removeMetadata($content) {
        $lines = explode("\n", $content);

        if (count($lines) > 0 && trim($lines[0]) === '---') {
            array_shift($lines);
            while (count($lines) > 0 && trim($lines[0]) !== '---') {
                array_shift($lines);
            }
            array_shift($lines);
            return implode("\n", $lines);
        }

        return $content;
    }

    private function parseMarkdown($content) {
        // 使用Parsedown解析Markdown
        $htmlContent = $this->parsedown->text($content);

        // 不再使用正则表达式处理数学公式,交给MathJax处理
        return $htmlContent;
    }

    private function getExcerpt($content, $length = 150) {
        $content = strip_tags($content);
        if (strlen($content) > $length) {
            $content = substr($content, 0, $length) . '...';
        }
        return $content;
    }

    private function searchArticles($query) {
        $results = [];

        foreach ($this->articles as $id => $article) {
            $content = strip_tags($article['content']);
            $title = $article['title'];

            // 简单的内容搜索
            $titleScore = substr_count(strtolower($title), strtolower($query)) * 10;
            $contentScore = substr_count(strtolower($content), strtolower($query));
            $totalScore = $titleScore + $contentScore;

            if ($totalScore > 0) {
                $snippet = $this->extractSnippet($content, $query);
                $results[$id] = [
                    'article' => $article,
                    'score' => $totalScore,
                    'snippet' => $snippet
                ];
            }
        }

        // 按匹配度排序
        uasort($results, function($a, $b) {
            return $b['score'] - $a['score'];
        });

        return $results;
    }

    private function extractSnippet($content, $query, $length = 200) {
        $position = stripos($content, $query);

        if ($position === false) {
            return substr($content, 0, $length) . '...';
        }

        $start = max(0, $position - $length / 2);
        $snippet = substr($content, $start, $length);

        if ($start > 0) {
            $snippet = '...' . $snippet;
        }

        if ($start + $length < strlen($content)) {
            $snippet = $snippet . '...';
        }

        // 高亮搜索词
        $snippet = preg_replace("/(" . preg_quote($query, '/') . ")/i", '<mark>$1</mark>', $snippet);

        return $snippet;
    }

    private function renderHomepage() {
        $this->renderHeader('首页');

        echo '
        <div class="container">
            <header class="page-header">
                <h1>'.SITE_TITLE.'</h1>
                <p>收录各类技术讨论精华文章</p>
            </header>

            <div class="search-box">
                <form action="" method="GET">
                    <input type="text" name="q" placeholder="搜索文章..." value="">
                    <button type="submit">搜索</button>
                </form>
            </div>

            <div class="archive-container">';

        foreach ($this->groupedArticles as $year => $yearData) {
            echo '
                <section class="year-section">
                    <h2 id="year-'.$year.'">'.$year.'年</h2>';

            foreach ($yearData['months'] as $month => $monthData) {
                echo '
                    <div class="month-section">
                        <h3>'.$monthData['month_name'].'</h3>

                        <div class="articles-list">';

                foreach ($monthData['articles'] as $id => $article) {
                    echo '
                            <article class="article-card">
                                <h4><a href="?page=article&id='.$article['id'].'">'.$article['title'].'</a></h4>
                                <div class="article-meta">
                                    <time datetime="'.$article['date'].'">'.date('Y年m月d日', strtotime($article['date'])).'</time>
                                    <span>•</span>
                                    <span>'.$article['author'].'</span>
                                </div>
                                <p class="article-excerpt">'.$this->getExcerpt($article['content'], 150).'</p>
                                <a href="?page=article&id='.$article['id'].'" class="read-more">阅读全文</a>
                            </article>';
                }

                echo '
                        </div>
                    </div>';
            }

            echo '
                </section>';
        }

        echo '
            </div>
        </div>';

        $this->renderFooter();
    }

    private function renderArticle($id) {
        $article = $this->articles[$id];
        $htmlContent = $this->parseMarkdown($article['content']);

        $this->renderHeader($article['title']);

        echo '
        <div class="container">
            <article class="article-full">
                <header class="article-header">
                    <h1>'.$article['title'].'</h1>
                    <div class="article-meta">
                        <time datetime="'.$article['date'].'">'.date('Y年m月d日', strtotime($article['date'])).'</time>
                        <span>•</span>
                        <span>'.$article['author'].'</span>
                    </div>
                </header>

                <div class="article-content">
                    '.$htmlContent.'
                </div>

                <footer class="article-footer">
                    <a href="./" class="back-link">← 返回首页</a>
                </footer>
            </article>
        </div>';

        $this->renderFooter();
    }

    private function renderSearchResults($query) {
        $results = $this->searchArticles($query);

        $this->renderHeader('搜索: ' . $query);

        echo '
        <div class="container">
            <header class="page-header">
                <h1>搜索: '.htmlspecialchars($query).'</h1>
                <p>找到 '.count($results).' 条结果</p>
            </header>

            <div class="search-results">';

        if (count($results) > 0) {
            foreach ($results as $result) {
                $article = $result['article'];
                echo '
                <article class="search-result">
                    <h3><a href="?page=article&id='.$article['id'].'">'.$article['title'].'</a></h3>
                    <div class="article-meta">
                        <time datetime="'.$article['date'].'">'.date('Y年m月d日', strtotime($article['date'])).'</time>
                        <span>•</span>
                        <span>'.$article['author'].'</span>
                    </div>
                    <p>'.$result['snippet'].'</p>
                </article>';
            }
        } else {
            echo '<p class="no-results">没有找到相关结果</p>';
        }

        echo '
            </div>
        </div>';

        $this->renderFooter();
    }

    private function renderHeader($title = '') {
        $fullTitle = $title ? $title . ' - ' . SITE_TITLE : SITE_TITLE;

        echo '<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>'.htmlspecialchars($fullTitle).'</title>
    <script>
        window.MathJax = {
            tex: {
                inlineMath: [[\'$\', \'$\']],
                displayMath: [[\'$$\', \'$$\']],
                processEscapes: true,
                processEnvironments: true
            },
            options: {
                skipHtmlTags: [\'script\', \'noscript\', \'style\', \'textarea\', \'pre\'],
                ignoreHtmlClass: \'no-mathjax\'
            }
        };
    </script>
    <script src="https://cdn.bootcss.com/mathjax/2.7.7/MathJax.js?config=TeX-AMS-MML_HTMLorMML-full"></script><link rel="stylesheet" href="/js/styles/tomorrow-night.css" /><script src="/js/highlight.pack.js"></script><script  type="text/javascript" language="javascript">hljs.initHighlightingOnLoad();</script>
    <style>
        :root {
            --primary: '.THEME_COLOR.';
            --primary-light: '.$this->adjustColorLightness(THEME_COLOR, 0.8).';
            --secondary: #64748b;
            --accent: #f1f5f9;
            --light: #f8f9fa;
            --dark: #212529;
            --gray: #6c757d;
            --border: #e9ecef;
            --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
            --code-font: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
            --line-height: 1.2;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: var(--font-family);
            line-height: var(--line-height);
            color: var(--dark);
            background-color: #fff;
            padding-top: 70px;
        }

        a {
            color: var(--primary);
            text-decoration: none;
            transition: color 0.2s ease;
        }

        a:hover {
            color: var(--primary);
            text-decoration: underline;
        }
        img {
              width: 100%;
              height: 100%;
              object-fit: cover;
       }        

        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 0 1rem;
        }

        /* 导航栏样式 */
        .navbar {
            background: white;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            z-index: 1000;
            padding: 0.8rem 0;
            backdrop-filter: blur(8px);
            background-color: rgba(255, 255, 255, 0.95);
        }

        .nav-container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 0 1rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .nav-logo {
            font-size: 1.5rem;
            font-weight: bold;
            color: var(--primary);
        }

        .nav-menu {
            display: flex;
            list-style: none;
        }

        .nav-item {
            margin-left: 1.5rem;
            position: relative;
        }

        .nav-link {
            color: var(--dark);
            font-weight: 500;
        }

        .nav-link:hover {
            color: var(--primary);
        }

        /* 页面标题 */
        .page-header {
            text-align: center;
            margin-bottom: 2rem;
            padding: 1.5rem 0;
        }

        .page-header h1 {
            font-size: 2.2rem;
            margin-bottom: 0.5rem;
            color: var(--dark);
        }

        .page-header p {
            font-size: 1.1rem;
            color: var(--gray);
        }

        /* 搜索框 */
        .search-box {
            max-width: 600px;
            margin: 0 auto 2.5rem;
        }

        .search-box form {
            display: flex;
        }

        .search-box input {
            flex: 1;
            padding: 0.75rem 1rem;
            border: 1px solid var(--border);
            border-radius: 6px 0 0 6px;
            font-size: 1rem;
            font-family: var(--font-family);
            transition: border-color 0.2s ease;
        }

        .search-box input:focus {
            outline: none;
            border-color: var(--primary);
        }

        .search-box button {
            padding: 0.75rem 1.5rem;
            background: var(--primary);
            color: white;
            border: none;
            border-radius: 0 6px 6px 0;
            cursor: pointer;
            font-size: 1rem;
            font-family: var(--font-family);
            transition: background-color 0.2s ease;
        }

        .search-box button:hover {
            background-color: '.$this->adjustColorLightness(THEME_COLOR, -0.1).';
        }

        /* 文章列表 */
        .archive-container {
            margin-bottom: 2.5rem;
        }

        .year-section {
            margin-bottom: 2.5rem;
        }

        .year-section h2 {
            font-size: 1.6rem;
            margin-bottom: 1.2rem;
            padding-bottom: 0.5rem;
            border-bottom: 2px solid var(--accent);
            color: var(--dark);
        }

        .month-section {
            margin-bottom: 1.8rem;
        }

        .month-section h3 {
            font-size: 1.3rem;
            margin-bottom: 0.8rem;
            color: var(--secondary);
        }

        .articles-list {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
            gap: 1.2rem;
        }

        .article-card {
            background: white;
            border-radius: 10px;
            padding: 1.4rem;
            box-shadow: 0 3px 12px rgba(0,0,0,0.06);
            transition: transform 0.2s, box-shadow 0.2s;
            border: 1px solid var(--border);
        }

        .article-card:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 18px rgba(0,0,0,0.1);
        }

        .article-card h4 {
            font-size: 1.15rem;
            margin-bottom: 0.6rem;
            line-height: 1.3;
        }

        .article-meta {
            font-size: 0.85rem;
            color: var(--gray);
            margin-bottom: 0.8rem;
            display: flex;
            align-items: center;
            gap: 0.4rem;
        }

        .article-excerpt {
            color: var(--secondary);
            margin-bottom: 1rem;
            font-size: 0.95rem;
            line-height: 1.4;
        }

        .read-more {
            font-weight: 500;
            font-size: 0.9rem;
            display: inline-flex;
            align-items: center;
            gap: 0.3rem;
        }

        .read-more:hover {
            text-decoration: none;
            color: '.$this->adjustColorLightness(THEME_COLOR, -0.2).';
        }

        /* 文章详情页 */
        .article-full {
            max-width: 800px;
            margin: 0 auto;
        }

        .article-header {
            text-align: center;
            margin-bottom: 1.8rem;
            padding-bottom: 1.5rem;
            border-bottom: 1px solid var(--border);
        }

        .article-header h1 {
            font-size: 2rem;
            margin-bottom: 0.8rem;
            line-height: 1.3;
        }

        .article-content {
            line-height: 1.4;
            font-size: 1.05rem;
        }

        .article-content h1, .article-content h2, .article-content h3 {
            margin: 1.8rem 0 1rem;
            color: var(--dark);
            line-height: 1.3;
        }

        .article-content h1 {
            font-size: 1.8rem;
            padding-bottom: 0.5rem;
            border-bottom: 1px solid var(--border);
        }

        .article-content h2 {
            font-size: 1.5rem;
        }

        .article-content h3 {
            font-size: 1.3rem;
        }

        .article-content p {
            margin-bottom: 1.2rem;
            line-height: 1.4;
        }

        .article-content ul, .article-content ol {
            margin-bottom: 1.2rem;
            padding-left: 1.5rem;
        }

        .article-content li {
            margin-bottom: 0.4rem;
        }

        .article-content pre {
            background: #f8f9fa;
            border-radius: 6px;
            overflow-x: auto;
            padding: 1.2rem;
            margin: 1.2rem 0;
            border: 1px solid var(--border);
            font-family: var(--code-font);
            font-size: 0.9rem;
            line-height: 1.4;
        }

        .article-content code {
            background: #f8f9fa;
            padding: 0.2rem 0.4rem;
            border-radius: 4px;
            font-family: var(--code-font);
            font-size: 0.9rem;
        }

        .article-content .inline-code {
            background: #f1f3f5;
            padding: 0.15rem 0.4rem;
            border-radius: 4px;
            font-size: 0.85rem;
            color: #e67700;
        }

        .article-content blockquote {
            border-left: 4px solid var(--primary);
            padding: 0.8rem 1rem;
            margin: 1.2rem 0;
            background-color: var(--primary-light);
            border-radius: 0 6px 6px 0;
        }

        .article-content blockquote p {
            margin-bottom: 0;
            color: #495057;
        }

        .md-link {
            color: var(--primary);
            text-decoration: underline;
            text-underline-offset: 2px;
        }

        .md-image {
            margin: 1.5rem 0;
            text-align: center;
        }

        .md-image img {
            max-width: 100%;
            height: auto;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
        }

        .image-caption {
            display: block;
            margin-top: 0.6rem;
            font-size: 0.9rem;
            color: var(--gray);
            font-style: italic;
        }

        /* MathJax样式调整 */
        .MathJax {
            font-size: 1.1em !important;
        }

        .mjx-chtml {
            line-height: 1.2 !important;
        }

        .article-footer {
            margin-top: 2.5rem;
            padding-top: 1.5rem;
            border-top: 1px solid var(--border);
        }

        .back-link {
            display: inline-flex;
            align-items: center;
            gap: 0.4rem;
            font-weight: 500;
            padding: 0.6rem 1rem;
            background: var(--accent);
            border-radius: 6px;
            transition: background-color 0.2s ease;
        }

        .back-link:hover {
            background: #e9ecef;
            text-decoration: none;
        }

        /* 搜索页面 */
        .search-results {
            max-width: 800px;
            margin: 0 auto;
        }

        .search-result {
            margin-bottom: 1.8rem;
            padding-bottom: 1.8rem;
            border-bottom: 1px solid var(--border);
        }

        .search-result h3 {
            font-size: 1.2rem;
            margin-bottom: 0.5rem;
            color: var(--dark);
        }

        .search-result .article-meta {
            margin-bottom: 0.5rem;
        }

        .no-results {
            text-align: center;
            color: var(--gray);
            font-size: 1.1rem;
            margin: 2.5rem 0;
        }

        /* 代码高亮增强 */
        pre code.hljs {
            padding: 0;
            background: transparent;
        }

        .language-php, .language-js, .language-css, .language-html, .language-py {
            position: relative;
        }

        .language-php::before, .language-js::before, .language-css::before, .language-html::before, .language-py::before {
            content: attr(class);
            position: absolute;
            top: 0;
            right: 0;
            padding: 0.2rem 0.6rem;
            font-size: 0.7rem;
            background: var(--primary);
            color: white;
            border-radius: 0 6px 0 6px;
        }

        /* 响应式设计 */
        @media (max-width: 768px) {
            body {
                padding-top: 60px;
            }

            .nav-menu {
                display: none;
            }

            .articles-list {
                grid-template-columns: 1fr;
            }

            .page-header h1 {
                font-size: 1.8rem;
            }

            .article-header h1 {
                font-size: 1.6rem;
            }

            .article-content {
                font-size: 1rem;
            }

            .article-content h1 {
                font-size: 1.5rem;
            }

            .article-content h2 {
                font-size: 1.3rem;
            }

            .article-content h3 {
                font-size: 1.15rem;
            }

            /* 移动端MathJax调整 */
            .MathJax {
                font-size: 1em !important;
            }

            mjx-container {
                overflow-x: auto;
                overflow-y: hidden;
            }
        }
    </style>
</head>
<body>
    <nav class="navbar">
        <div class="nav-container">
            <a href="./" class="nav-logo">'.SITE_TITLE.'</a>
            <ul class="nav-menu">
                <li class="nav-item"><a href="./" class="nav-link">首页</a></li>';

        foreach ($this->groupedArticles as $year => $yearData) {
            echo '<li class="nav-item"><a href="#year-'.$year.'" class="nav-link">'.$year.'年</a></li>';
        }

        echo '
            </ul>
        </div>
    </nav>';
    }

    private function renderFooter() {
        echo '
</body>
</html>';
    }

    private function adjustColorLightness($color, $amount) {
        // 调整颜色亮度的辅助函数
        $hex = str_replace('#', '', $color);

        if (strlen($hex) == 3) {
            $hex = str_repeat(substr($hex,0,1), 2).str_repeat(substr($hex,1,1), 2).str_repeat(substr($hex,2,1), 2);
        }

        $r = hexdec(substr($hex,0,2));
        $g = hexdec(substr($hex,2,2));
        $b = hexdec(substr($hex,4,2));

        $r = max(0, min(255, $r + $amount * 255));
        $g = max(0, min(255, $g + $amount * 255));
        $b = max(0, min(255, $b + $amount * 255));

        return '#'.str_pad(dechex($r), 2, '0', STR_PAD_LEFT)
                  .str_pad(dechex($g), 2, '0', STR_PAD_LEFT)
                  .str_pad(dechex($b), 2, '0', STR_PAD_LEFT);
    }
}
?>