前言

在前两章中,我们学习了:

  • 第一章:Hexo 的核心概念(事件、变量、路由、Box、渲染、文章、模板)
  • 第二章:Hexo 的扩展系统(Console、Deployer、Filter、Generator、Helper、Injector 等)

现在,是时候将理论付诸实践了。本章我们将学习如何构建一个完整的 Hexo 主题,包括:

  • 🏗️ 主题目录结构 - 组织主题文件的最佳实践
  • 📐 Layout 布局系统 - 构建页面框架
  • 🧩 Partial 局部模板 - 模块化和复用
  • 🎨 样式组织 - CSS/SCSS 架构设计
  • 脚本管理 - JavaScript 的组织与优化
  • 🔧 配置系统 - 主题配置的设计
  • 📱 响应式设计 - 适配不同设备

本章将带你从零开始构建一个名为 “Aurora” 的现代化 Hexo 主题。


第一部分:主题目录结构设计

标准目录结构

一个完整的 Hexo 主题目录结构如下:

themes/aurora/
├── _config.yml              # 主题配置文件
├── languages/               # 国际化语言文件
│   ├── zh-CN.yml
│   ├── en.yml
│   └── ja.yml
├── layout/                  # 布局模板
│   ├── _partial/           # 局部模板
│   │   ├── head.ejs
│   │   ├── header.ejs
│   │   ├── footer.ejs
│   │   ├── sidebar.ejs
│   │   ├── article.ejs
│   │   ├── comment.ejs
│   │   └── widget/         # 小部件
│   │       ├── category.ejs
│   │       ├── tag.ejs
│   │       ├── recent-posts.ejs
│   │       └── archive.ejs
│   ├── _widget/            # 独立小部件(可选)
│   ├── index.ejs           # 首页布局
│   ├── post.ejs            # 文章页布局
│   ├── page.ejs            # 独立页面布局
│   ├── archive.ejs         # 归档页布局
│   ├── category.ejs        # 分类页布局
│   ├── tag.ejs             # 标签页布局
│   └── layout.ejs          # 基础布局(所有页面的父模板)
├── source/                  # 静态资源
│   ├── css/                # 样式文件
│   │   ├── _variables.scss # 变量定义
│   │   ├── _mixins.scss    # Mixins
│   │   ├── _base.scss      # 基础样式
│   │   ├── _layout.scss    # 布局样式
│   │   ├── _components.scss# 组件样式
│   │   ├── _utilities.scss # 工具类
│   │   └── style.scss      # 主样式文件
│   ├── js/                 # JavaScript 文件
│   │   ├── utils.js        # 工具函数
│   │   ├── main.js         # 主脚本
│   │   ├── search.js       # 搜索功能
│   │   └── components/     # 组件脚本
│   │       ├── toc.js
│   │       ├── lazyload.js
│   │       └── theme-toggle.js
│   ├── images/             # 图片资源
│   │   ├── logo.svg
│   │   ├── avatar-default.png
│   │   └── icons/
│   └── fonts/              # 字体文件
├── scripts/                 # Hexo 扩展脚本
│   ├── helpers/            # Helper 函数
│   │   ├── format.js
│   │   └── components.js
│   ├── filters/            # Filter 函数
│   │   ├── post.js
│   │   └── assets.js
│   ├── tags/               # 自定义标签
│   │   ├── note.js
│   │   └── tabs.js
│   └── generators/         # 生成器
│       └── search.js
└── package.json            # 依赖管理(可选)

创建主题骨架

让我们开始创建主题的基本结构:

# 创建主题目录
cd themes
mkdir aurora
cd aurora

# 创建主要目录
mkdir -p layout/_partial/widget
mkdir -p source/{css,js/components,images/icons,fonts}
mkdir -p scripts/{helpers,filters,tags,generators}
mkdir -p languages

# 创建配置文件
touch _config.yml

主题配置文件设计

themes/aurora/_config.yml

# ============================================
# Aurora 主题配置文件
# ============================================

# 站点基本信息
# --------------------------------------------
site:
  name: Aurora Theme
  subtitle: 一个现代化的 Hexo 主题
  description: 简洁、优雅、功能丰富
  keywords: hexo, theme, aurora, blog
  author: Your Name
  avatar: /images/avatar.jpg
  favicon: /images/favicon.ico

# 导航菜单
# --------------------------------------------
menu:
  home:
    name: 首页
    path: /
    icon: home
  archives:
    name: 归档
    path: /archives/
    icon: archive
  categories:
    name: 分类
    path: /categories/
    icon: folder
  tags:
    name: 标签
    path: /tags/
    icon: tag
  about:
    name: 关于
    path: /about/
    icon: user

# 社交链接
# --------------------------------------------
social:
  github: https://github.com/username
  twitter: https://twitter.com/username
  email: mailto:your@email.com
  rss: /atom.xml

# 外观设置
# --------------------------------------------
appearance:
  # 主题色
  theme_color: '#4dabf7'
  
  # 暗色模式
  dark_mode:
    enable: true
    default: auto  # auto, light, dark
    
  # 代码高亮
  code:
    theme: tomorrow-night
    line_numbers: true
    copy_button: true
    
  # 字体
  font:
    enable: true
    host: //fonts.googleapis.com
    family:
      sans_serif: 'Noto Sans SC, sans-serif'
      monospace: 'Fira Code, monospace'

# 侧边栏
# --------------------------------------------
sidebar:
  enable: true
  position: right  # left, right
  
  # 侧边栏小部件
  widgets:
    - profile      # 个人信息
    - toc         # 文章目录
    - recent      # 最近文章
    - categories  # 分类
    - tags        # 标签云
    - archive     # 归档

# 文章设置
# --------------------------------------------
post:
  # 元信息显示
  meta:
    date: true
    updated: true
    categories: true
    tags: true
    word_count: true
    reading_time: true
    
  # 文章封面
  cover:
    enable: true
    default: /images/default-cover.jpg
    
  # 版权信息
  copyright:
    enable: true
    license: CC BY-NC-SA 4.0
    
  # 相关文章
  related_posts:
    enable: true
    limit: 5
    
  # 打赏
  reward:
    enable: false
    comment: 如果觉得文章有帮助,可以请我喝杯咖啡
    methods:
      - name: 微信
        qrcode: /images/wechat-qr.jpg
      - name: 支付宝
        qrcode: /images/alipay-qr.jpg

# 评论系统
# --------------------------------------------
comments:
  enable: true
  provider: gitalk  # disqus, gitalk, valine, waline
  
  # Gitalk 配置
  gitalk:
    client_id: your_client_id
    client_secret: your_client_secret
    repo: blog-comments
    owner: username
    admin:
      - username
      
  # Disqus 配置
  disqus:
    shortname: your_shortname
    
  # Valine 配置
  valine:
    app_id: your_app_id
    app_key: your_app_key
    placeholder: 说点什么吧...
    avatar: mp
    page_size: 10

# 搜索功能
# --------------------------------------------
search:
  enable: true
  provider: local  # local, algolia
  
  # 本地搜索
  local:
    trigger: manual  # auto, manual
    top_n_per_article: 1
    
  # Algolia 搜索
  algolia:
    application_id: your_app_id
    api_key: your_api_key
    index_name: your_index_name

# 分析统计
# --------------------------------------------
analytics:
  enable: true
  
  # Google Analytics
  google_analytics:
    enable: false
    tracking_id: UA-XXXXXXXXX-X
    
  # 百度统计
  baidu_analytics:
    enable: false
    tracking_id: your_tracking_id
    
  # 站长验证
  site_verification:
    google: your_verification_code
    baidu: your_verification_code

# 性能优化
# --------------------------------------------
optimization:
  # 静态资源 CDN
  cdn:
    enable: false
    provider: jsdelivr  # jsdelivr, unpkg, custom
    
  # 图片懒加载
  lazyload:
    enable: true
    placeholder: /images/loading.svg
    
  # 预加载
  preload:
    enable: true
    
  # PWA
  pwa:
    enable: false
    manifest: /manifest.json
    
  # 压缩
  minify:
    html: true
    css: true
    js: true

# 高级功能
# --------------------------------------------
features:
  # 目录(TOC)
  toc:
    enable: true
    number: true
    max_depth: 3
    
  # 图片灯箱
  fancybox:
    enable: true
    
  # 数学公式
  mathjax:
    enable: false
    per_page: false
    
  # 图表
  mermaid:
    enable: false
    
  # 音乐播放器
  music:
    enable: false
    
  # 阅读进度条
  reading_progress:
    enable: true
    position: top  # top, bottom
    
  # 返回顶部
  back_to_top:
    enable: true
    show_percent: true

# 版权声明
# --------------------------------------------
footer:
  # 建站时间
  since: 2020
  
  # 自定义文本
  custom_text: |
    Powered by <a href="https://hexo.io" target="_blank">Hexo</a>
    Theme - <a href="https://github.com/username/hexo-theme-aurora" target="_blank">Aurora</a>
    
  # 备案信息
  icp:
    enable: false
    number: 京ICP备xxxxxx号
    url: https://beian.miit.gov.cn/
    
  # 页脚链接
  links:
    - name: 友情链接
      url: /links/
    - name: RSS
      url: /atom.xml

# 开发选项
# --------------------------------------------
development:
  # 调试模式
  debug: false
  
  # 显示性能指标
  performance: false

配置文件最佳实践

// scripts/helpers/config.js
// 提供便捷的配置访问 Helper

hexo.extend.helper.register('theme_config', function(path, defaultValue) {
  // 使用点号分隔的路径访问嵌套配置
  const keys = path.split('.');
  let value = this.theme;
  
  for (const key of keys) {
    if (value && typeof value === 'object' && key in value) {
      value = value[key];
    } else {
      return defaultValue;
    }
  }
  
  return value !== undefined ? value : defaultValue;
});

// 使用示例:
// <%- theme_config('appearance.dark_mode.enable', false) %>
// <%- theme_config('comments.provider', 'none') %>

第二部分:Layout 布局系统

布局继承体系

Hexo 的布局系统采用继承模式,子布局可以扩展父布局:

layout.ejs (基础布局)

    ├─ index.ejs (首页)
    ├─ post.ejs (文章页)
    ├─ page.ejs (独立页面)
    ├─ archive.ejs (归档页)
    ├─ category.ejs (分类页)
    └─ tag.ejs (标签页)

基础布局模板

themes/aurora/layout/layout.ejs

<!DOCTYPE html>
<html lang="<%= config.language || 'zh-CN' %>">
<%- partial('_partial/head') %>
<body>
  <!-- 页面加载动画 -->
  <div id="loading" class="loading">
    <div class="spinner"></div>
  </div>
  
  <!-- 页面容器 -->
  <div id="container" class="container">
    <!-- 顶部导航 -->
    <%- partial('_partial/header') %>
    
    <!-- 主要内容区 -->
    <main id="main" class="main">
      <div class="content-wrapper">
        <!-- 这里是子布局的内容插入点 -->
        <%- body %>
      </div>
    </main>
    
    <!-- 侧边栏 -->
    <% if (theme_config('sidebar.enable', true)) { %>
      <%- partial('_partial/sidebar') %>
    <% } %>
    
    <!-- 底部 -->
    <%- partial('_partial/footer') %>
  </div>
  
  <!-- 返回顶部按钮 -->
  <% if (theme_config('features.back_to_top.enable', true)) { %>
    <%- partial('_partial/back-to-top') %>
  <% } %>
  
  <!-- 搜索框 -->
  <% if (theme_config('search.enable', false)) { %>
    <%- partial('_partial/search') %>
  <% } %>
  
  <!-- 脚本文件 -->
  <%- partial('_partial/scripts') %>
</body>
</html>

首页布局

themes/aurora/layout/index.ejs

<!-- 首页轮播(可选) -->
<% if (theme_config('home.banner.enable', false)) { %>
  <section class="hero-banner">
    <%- partial('_partial/banner') %>
  </section>
<% } %>

<!-- 文章列表 -->
<section class="post-list">
  <div class="post-container">
    <% page.posts.each(function(post) { %>
      <%- partial('_partial/article', {
        post: post,
        index: true
      }) %>
    <% }) %>
  </div>
  
  <!-- 分页导航 -->
  <% if (page.total > 1) { %>
    <%- partial('_partial/pagination') %>
  <% } %>
</section>

<!-- 推荐文章(可选) -->
<% if (theme_config('home.featured.enable', false)) { %>
  <aside class="featured-posts">
    <%- partial('_partial/featured-posts') %>
  </aside>
<% } %>

文章页布局

themes/aurora/layout/post.ejs

<article class="post-detail">
  <!-- 文章头部 -->
  <header class="post-header">
    <!-- 封面图 -->
    <% if (page.cover || theme_config('post.cover.enable', false)) { %>
      <div class="post-cover">
        <img 
          src="<%= page.cover || theme_config('post.cover.default') %>" 
          alt="<%= page.title %>"
          loading="lazy"
        >
      </div>
    <% } %>
    
    <!-- 标题和元信息 -->
    <div class="post-header-info">
      <h1 class="post-title"><%= page.title %></h1>
      
      <%- partial('_partial/post-meta', {
        post: page,
        class: 'post-meta'
      }) %>
    </div>
  </header>
  
  <!-- 文章内容 -->
  <div class="post-content-wrapper">
    <!-- 目录 -->
    <% if (theme_config('features.toc.enable', true) && page.toc !== false) { %>
      <aside class="post-toc">
        <%- partial('_partial/toc', {content: page.content}) %>
      </aside>
    <% } %>
    
    <!-- 正文 -->
    <div class="post-content markdown-body">
      <%- page.content %>
    </div>
  </div>
  
  <!-- 文章底部信息 -->
  <footer class="post-footer">
    <!-- 标签 -->
    <% if (page.tags && page.tags.length) { %>
      <div class="post-tags">
        <i class="icon-tag"></i>
        <% page.tags.each(function(tag) { %>
          <a href="<%- url_for(tag.path) %>" class="tag-link">
            <%= tag.name %>
          </a>
        <% }) %>
      </div>
    <% } %>
    
    <!-- 版权声明 -->
    <% if (theme_config('post.copyright.enable', false)) { %>
      <%- partial('_partial/copyright') %>
    <% } %>
    
    <!-- 分享按钮 -->
    <% if (theme_config('post.share.enable', false)) { %>
      <%- partial('_partial/share') %>
    <% } %>
    
    <!-- 打赏 -->
    <% if (theme_config('post.reward.enable', false)) { %>
      <%- partial('_partial/reward') %>
    <% } %>
  </footer>
  
  <!-- 文章导航(上一篇/下一篇) -->
  <%- partial('_partial/post-nav') %>
  
  <!-- 相关文章 -->
  <% if (theme_config('post.related_posts.enable', false)) { %>
    <%- partial('_partial/related-posts') %>
  <% } %>
  
  <!-- 评论 -->
  <% if (theme_config('comments.enable', false) && page.comments !== false) { %>
    <%- partial('_partial/comments') %>
  <% } %>
</article>

归档页布局

themes/aurora/layout/archive.ejs

<div class="archive-page">
  <!-- 页面标题 -->
  <header class="page-header">
    <h1 class="page-title">
      <% if (page.year) { %>
        <%= page.year %>
        <% if (page.month) { %>
          - <%= page.month %>
        <% } %>
        年归档
      <% } else if (page.category) { %>
        分类:<%= page.category %>
      <% } else if (page.tag) { %>
        标签:<%= page.tag %>
      <% } else { %>
        归档
      <% } %>
    </h1>
    
    <% if (page.total) { %>
      <div class="page-subtitle">
<%= page.total %> 篇文章
      </div>
    <% } %>
  </header>
  
  <!-- 归档统计 -->
  <% if (!page.year && !page.category && !page.tag) { %>
    <section class="archive-stats">
      <%- partial('_partial/archive-stats') %>
    </section>
  <% } %>
  
  <!-- 文章列表 -->
  <section class="archive-list">
    <%- partial('_partial/archive-list', {
      posts: page.posts
    }) %>
  </section>
  
  <!-- 分页 -->
  <% if (page.total > 1) { %>
    <%- partial('_partial/pagination') %>
  <% } %>
</div>

独立页面布局

themes/aurora/layout/page.ejs

<article class="page-detail">
  <header class="page-header">
    <h1 class="page-title"><%= page.title %></h1>
    
    <% if (page.subtitle) { %>
      <p class="page-subtitle"><%= page.subtitle %></p>
    <% } %>
  </header>
  
  <div class="page-content markdown-body">
    <%- page.content %>
  </div>
  
  <!-- 评论(如果页面开启) -->
  <% if (theme_config('comments.enable', false) && page.comments !== false) { %>
    <section class="page-comments">
      <%- partial('_partial/comments') %>
    </section>
  <% } %>
</article>

第三部分:Partial 局部模板

Head 头部模板

themes/aurora/layout/_partial/head.ejs

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
  
  <!-- SEO Meta -->
  <%- partial('_partial/seo') %>
  
  <!-- Favicon -->
  <% if (theme_config('site.favicon')) { %>
    <link rel="icon" href="<%- url_for(theme_config('site.favicon')) %>">
  <% } %>
  
  <!-- 主题色 -->
  <meta name="theme-color" content="<%= theme_config('appearance.theme_color', '#4dabf7') %>">
  
  <!-- 预加载关键资源 -->
  <% if (theme_config('optimization.preload.enable', false)) { %>
    <link rel="preload" href="<%- url_for('/css/style.css') %>" as="style">
    <link rel="preload" href="<%- url_for('/js/main.js') %>" as="script">
  <% } %>
  
  <!-- 样式文件 -->
  <%- css('css/style') %>
  
  <!-- 自定义 CSS -->
  <% if (theme_config('custom.css')) { %>
    <style><%- theme_config('custom.css') %></style>
  <% } %>
  
  <!-- 字体 -->
  <% if (theme_config('appearance.font.enable', false)) { %>
    <link rel="preconnect" href="<%= theme_config('appearance.font.host') %>">
    <link href="<%= theme_config('appearance.font.host') %>/css2?family=<%= theme_config('appearance.font.family.sans_serif') %>&display=swap" rel="stylesheet">
  <% } %>
  
  <!-- 暗色模式初始化脚本(避免闪烁) -->
  <% if (theme_config('appearance.dark_mode.enable', false)) { %>
    <script>
      (function() {
        const theme = localStorage.getItem('theme') || '<%= theme_config('appearance.dark_mode.default', 'auto') %>';
        if (theme === 'dark' || (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
          document.documentElement.setAttribute('data-theme', 'dark');
        }
      })();
    </script>
  <% } %>
</head>

SEO 优化模板

themes/aurora/layout/_partial/seo.ejs

<!-- 标题 -->
<% 
  let pageTitle = page.title || config.title;
  if (is_archive()) {
    pageTitle = '归档';
    if (page.year) pageTitle += ' - ' + page.year;
    if (page.month) pageTitle += '/' + page.month;
  } else if (is_category()) {
    pageTitle = '分类:' + page.category;
  } else if (is_tag()) {
    pageTitle = '标签:' + page.tag;
  }
  if (page.current > 1) {
    pageTitle += ' - 第' + page.current + '页';
  }
%>
<title><%= pageTitle %><% if (!is_home() && config.title) { %> - <%= config.title %><% } %></title>

<!-- 描述 -->
<%
  let description = page.description || page.excerpt || page.content;
  if (description) {
    description = strip_html(description).substring(0, 200)
      .trim().replace(/\s+/g, ' ');
  } else {
    description = config.description;
  }
%>
<meta name="description" content="<%= description %>">

<!-- 关键词 -->
<%
  let keywords = [];
  if (page.keywords) {
    keywords = page.keywords;
  } else if (page.tags && page.tags.length) {
    keywords = page.tags.map(tag => tag.name);
  } else if (config.keywords) {
    keywords = config.keywords;
  }
%>
<% if (keywords.length) { %>
  <meta name="keywords" content="<%= Array.isArray(keywords) ? keywords.join(',') : keywords %>">
<% } %>

<!-- 作者 -->
<meta name="author" content="<%= page.author || config.author || theme_config('site.author') %>">

<!-- Open Graph -->
<meta property="og:type" content="<%= is_post() ? 'article' : 'website' %>">
<meta property="og:title" content="<%= pageTitle %>">
<meta property="og:url" content="<%= url %>">
<meta property="og:site_name" content="<%= config.title %>">
<meta property="og:description" content="<%= description %>">
<meta property="og:locale" content="<%= config.language.replace('-', '_') %>">

<!-- 文章专属 -->
<% if (is_post()) { %>
  <meta property="article:published_time" content="<%= page.date.toISOString() %>">
  <meta property="article:modified_time" content="<%= page.updated.toISOString() %>">
  <% if (page.cover) { %>
    <meta property="og:image" content="<%= url_for(page.cover) %>">
  <% } %>
  <% if (page.tags && page.tags.length) { %>
    <% page.tags.each(tag => { %>
      <meta property="article:tag" content="<%= tag.name %>">
    <% }) %>
  <% } %>
<% } %>

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="<%= pageTitle %>">
<meta name="twitter:description" content="<%= description %>">
<% if (config.twitter) { %>
  <meta name="twitter:creator" content="@<%= config.twitter %>">
<% } %>
<% if (is_post() && page.cover) { %>
  <meta name="twitter:image" content="<%= url_for(page.cover) %>">
<% } %>

<!-- Canonical URL -->
<link rel="canonical" href="<%= url %>">

<!-- Feed -->
<% if (config.feed && config.feed.path) { %>
  <link rel="alternate" href="<%= url_for(config.feed.path) %>" title="<%= config.title %>" type="application/atom+xml">
<% } %>

Header 顶部导航

themes/aurora/layout/_partial/header.ejs

<header class="site-header" id="site-header">
  <div class="header-container">
    <!-- Logo 和站点标题 -->
    <div class="site-branding">
      <a href="<%= url_for('/') %>" class="site-logo-link">
        <% if (theme_config('site.logo')) { %>
          <img src="<%- url_for(theme_config('site.logo')) %>" alt="<%= config.title %>" class="site-logo">
        <% } %>
        <span class="site-title"><%= config.title %></span>
      </a>
      
      <% if (config.subtitle) { %>
        <p class="site-subtitle"><%= config.subtitle %></p>
      <% } %>
    </div>
    
    <!-- 导航菜单 -->
    <nav class="site-nav" id="site-nav">
      <ul class="nav-menu">
        <% 
          const menu = theme_config('menu', {});
          Object.keys(menu).forEach(key => {
            const item = menu[key];
            const isActive = is_current(item.path);
        %>
          <li class="nav-item <%= isActive ? 'active' : '' %>">
            <a href="<%- url_for(item.path) %>" class="nav-link">
              <% if (item.icon) { %>
                <i class="icon-<%= item.icon %>"></i>
              <% } %>
              <span><%= item.name %></span>
            </a>
          </li>
        <% }) %>
      </ul>
    </nav>
    
    <!-- 右侧工具栏 -->
    <div class="header-tools">
      <!-- 搜索按钮 -->
      <% if (theme_config('search.enable', false)) { %>
        <button class="tool-button search-button" id="search-button" aria-label="搜索">
          <i class="icon-search"></i>
        </button>
      <% } %>
      
      <!-- 暗色模式切换 -->
      <% if (theme_config('appearance.dark_mode.enable', false)) { %>
        <button class="tool-button theme-toggle" id="theme-toggle" aria-label="切换主题">
          <i class="icon-sun sun-icon"></i>
          <i class="icon-moon moon-icon"></i>
        </button>
      <% } %>
      
      <!-- 移动端菜单按钮 -->
      <button class="tool-button menu-toggle" id="menu-toggle" aria-label="菜单">
        <span class="hamburger">
          <span></span>
          <span></span>
          <span></span>
        </span>
      </button>
    </div>
  </div>
  
  <!-- 阅读进度条 -->
  <% if (is_post() && theme_config('features.reading_progress.enable', false)) { %>
    <div class="reading-progress-bar" id="reading-progress"></div>
  <% } %>
</header>

Article 文章摘要

themes/aurora/layout/_partial/article.ejs

<%
  const isIndex = index !== undefined ? index : false;
  const showExcerpt = isIndex && theme_config('home.excerpt.enable', true);
%>

<article class="post-item <%= isIndex ? 'post-index' : 'post-full' %>">
  <!-- 封面图 -->
  <% if (post.cover || theme_config('post.cover.enable', false)) { %>
    <div class="post-cover">
      <a href="<%- url_for(post.path) %>">
        <img 
          src="<%= post.cover || theme_config('post.cover.default') %>" 
          alt="<%= post.title %>"
          loading="lazy"
        >
      </a>
      
      <% if (post.featured) { %>
        <span class="post-badge badge-featured">精选</span>
      <% } %>
      
      <% if (post.sticky) { %>
        <span class="post-badge badge-sticky">置顶</span>
      <% } %>
    </div>
  <% } %>
  
  <!-- 文章信息 -->
  <div class="post-info">
    <!-- 标题 -->
    <h2 class="post-title">
      <a href="<%- url_for(post.path) %>"><%= post.title %></a>
    </h2>
    
    <!-- 元信息 -->
    <%- partial('_partial/post-meta', {
      post: post,
      class: 'post-meta-simple'
    }) %>
    
    <!-- 摘要 -->
    <% if (showExcerpt) { %>
      <div class="post-excerpt">
        <% if (post.excerpt) { %>
          <%- post.excerpt %>
        <% } else { %>
          <%- truncate(strip_html(post.content), {
            length: theme_config('home.excerpt.length', 200)
          }) %>
        <% } %>
      </div>
      
      <a href="<%- url_for(post.path) %>" class="read-more">
        阅读全文 <i class="icon-arrow-right"></i>
      </a>
    <% } %>
    
    <!-- 首页显示标签 -->
    <% if (isIndex && post.tags && post.tags.length) { %>
      <div class="post-tags-simple">
        <% post.tags.limit(3).each(tag => { %>
          <a href="<%- url_for(tag.path) %>" class="tag-link">
            #<%= tag.name %>
          </a>
        <% }) %>
      </div>
    <% } %>
  </div>
</article>

Post Meta 文章元信息

themes/aurora/layout/_partial/post-meta.ejs

<div class="post-meta <%= class || '' %>">
  <!-- 发布日期 -->
  <% if (theme_config('post.meta.date', true)) { %>
    <span class="meta-item meta-date">
      <i class="icon-calendar"></i>
      <time datetime="<%= post.date.toISOString() %>">
        <%= date(post.date, 'YYYY-MM-DD') %>
      </time>
    </span>
  <% } %>
  
  <!-- 更新日期 -->
  <% if (theme_config('post.meta.updated', true) && post.updated && post.updated.valueOf() !== post.date.valueOf()) { %>
    <span class="meta-item meta-updated">
      <i class="icon-refresh"></i>
      <time datetime="<%= post.updated.toISOString() %>">
        <%= date(post.updated, 'YYYY-MM-DD') %>
      </time>
    </span>
  <% } %>
  
  <!-- 分类 -->
  <% if (theme_config('post.meta.categories', true) && post.categories && post.categories.length) { %>
    <span class="meta-item meta-categories">
      <i class="icon-folder"></i>
      <% post.categories.each(category => { %>
        <a href="<%- url_for(category.path) %>"><%= category.name %></a>
      <% }) %>
    </span>
  <% } %>
  
  <!-- 字数统计 -->
  <% if (theme_config('post.meta.word_count', true) && post.word_count) { %>
    <span class="meta-item meta-wordcount">
      <i class="icon-file-text"></i>
      <%= post.word_count %> 字
    </span>
  <% } %>
  
  <!-- 阅读时间 -->
  <% if (theme_config('post.meta.reading_time', true) && post.reading_time) { %>
    <span class="meta-item meta-reading-time">
      <i class="icon-clock"></i>
<%= post.reading_time %> 分钟
    </span>
  <% } %>
  
  <!-- 浏览次数(需要第三方服务) -->
  <% if (theme_config('post.meta.views', false)) { %>
    <span class="meta-item meta-views">
      <i class="icon-eye"></i>
      <span class="post-views" data-path="<%= post.path %>">-</span> 次浏览
    </span>
  <% } %>
</div>

themes/aurora/layout/_partial/sidebar.ejs

<aside class="sidebar" id="sidebar">
  <div class="sidebar-inner">
    <%
      const widgets = theme_config('sidebar.widgets', [
        'profile', 'toc', 'recent', 'categories', 'tags', 'archive'
      ]);
      
      widgets.forEach(widget => {
    %>
      <% if (widget === 'profile') { %>
        <%- partial('_partial/widget/profile') %>
      <% } else if (widget === 'toc' && is_post() && page.toc !== false) { %>
        <%- partial('_partial/widget/toc', {content: page.content}) %>
      <% } else if (widget === 'recent') { %>
        <%- partial('_partial/widget/recent-posts') %>
      <% } else if (widget === 'categories') { %>
        <%- partial('_partial/widget/categories') %>
      <% } else if (widget === 'tags') { %>
        <%- partial('_partial/widget/tags') %>
      <% } else if (widget === 'archive') { %>
        <%- partial('_partial/widget/archive') %>
      <% } %>
    <% }) %>
  </div>
</aside>

Widget - 个人信息

themes/aurora/layout/_partial/widget/profile.ejs

<div class="widget widget-profile">
  <div class="profile-avatar">
    <img 
      src="<%- url_for(theme_config('site.avatar', '/images/avatar.jpg')) %>" 
      alt="<%= config.author %>"
    >
  </div>
  
  <h3 class="profile-name"><%= config.author || theme_config('site.author') %></h3>
  
  <% if (config.description || theme_config('site.description')) { %>
    <p class="profile-bio"><%= config.description || theme_config('site.description') %></p>
  <% } %>
  
  <!-- 统计信息 -->
  <div class="profile-stats">
    <div class="stat-item">
      <span class="stat-value"><%= site.posts.length %></span>
      <span class="stat-label">文章</span>
    </div>
    <div class="stat-item">
      <span class="stat-value"><%= site.categories.length %></span>
      <span class="stat-label">分类</span>
    </div>
    <div class="stat-item">
      <span class="stat-value"><%= site.tags.length %></span>
      <span class="stat-label">标签</span>
    </div>
  </div>
  
  <!-- 社交链接 -->
  <%
    const social = theme_config('social', {});
    if (Object.keys(social).length > 0) {
  %>
    <div class="profile-social">
      <% Object.keys(social).forEach(key => { %>
        <a 
          href="<%= social[key] %>" 
          class="social-link social-<%= key %>"
          target="_blank"
          rel="noopener noreferrer"
          aria-label="<%= key %>"
        >
          <i class="icon-<%= key %>"></i>
        </a>
      <% }) %>
    </div>
  <% } %>
</div>

Widget - 最近文章

themes/aurora/layout/_partial/widget/recent-posts.ejs

<div class="widget widget-recent-posts">
  <h3 class="widget-title">最近文章</h3>
  <ul class="recent-posts-list">
    <% 
      const recentPosts = site.posts.sort('date', -1).limit(5);
      recentPosts.each(post => {
    %>
      <li class="recent-post-item">
        <% if (post.cover) { %>
          <a href="<%- url_for(post.path) %>" class="recent-post-thumb">
            <img src="<%= post.cover %>" alt="<%= post.title %>" loading="lazy">
          </a>
        <% } %>
        <div class="recent-post-info">
          <a href="<%- url_for(post.path) %>" class="recent-post-title">
            <%= post.title %>
          </a>
          <time class="recent-post-date">
            <%= date(post.date, 'MM-DD') %>
          </time>
        </div>
      </li>
    <% }) %>
  </ul>
</div>

Widget - 分类

themes/aurora/layout/_partial/widget/categories.ejs

<div class="widget widget-categories">
  <h3 class="widget-title">分类</h3>
  <ul class="category-list">
    <%
      const categories = site.categories.sort('name');
      categories.each(category => {
    %>
      <li class="category-item">
        <a href="<%- url_for(category.path) %>" class="category-link">
          <span class="category-name"><%= category.name %></span>
          <span class="category-count"><%= category.length %></span>
        </a>
      </li>
    <% }) %>
  </ul>
</div>

Widget - 标签云

themes/aurora/layout/_partial/widget/tags.ejs

<div class="widget widget-tags">
  <h3 class="widget-title">标签云</h3>
  <div class="tag-cloud">
    <%
      const tags = site.tags.sort('name');
      const maxCount = tags.first() ? tags.first().length : 1;
      const minCount = tags.last() ? tags.last().length : 1;
      
      tags.each(tag => {
        const weight = (tag.length - minCount) / (maxCount - minCount || 1);
        const fontSize = 12 + Math.floor(weight * 10);
    %>
      <a 
        href="<%- url_for(tag.path) %>" 
        class="tag-cloud-item"
        style="font-size: <%= fontSize %>px"
        title="<%= tag.name %> (<%= tag.length %>)"
      >
        <%= tag.name %>
      </a>
    <% }) %>
  </div>
</div>

themes/aurora/layout/_partial/footer.ejs

<footer class="site-footer">
  <div class="footer-container">
    <!-- 页脚信息 -->
    <div class="footer-info">
      <!-- 版权信息 -->
      <div class="copyright">
        &copy; 
        <% 
          const since = theme_config('footer.since');
          const currentYear = new Date().getFullYear();
        %>
        <% if (since && since < currentYear) { %>
          <%= since %> - <%= currentYear %>
        <% } else { %>
          <%= currentYear %>
        <% } %>
        <a href="<%= config.url %>"><%= config.author || config.title %></a>
      </div>
      
      <!-- 自定义文本 -->
      <% if (theme_config('footer.custom_text')) { %>
        <div class="footer-custom">
          <%- theme_config('footer.custom_text') %>
        </div>
      <% } %>
      
      <!-- 备案信息 -->
      <% if (theme_config('footer.icp.enable', false)) { %>
        <div class="footer-icp">
          <a 
            href="<%= theme_config('footer.icp.url') %>" 
            target="_blank"
            rel="noopener noreferrer"
          >
            <%= theme_config('footer.icp.number') %>
          </a>
        </div>
      <% } %>
    </div>
    
    <!-- 页脚链接 -->
    <%
      const footerLinks = theme_config('footer.links', []);
      if (footerLinks.length > 0) {
    %>
      <div class="footer-links">
        <% footerLinks.forEach(link => { %>
          <a 
            href="<%= link.url %>" 
            <% if (link.external !== false) { %>
              target="_blank" 
              rel="noopener noreferrer"
            <% } %>
          >
            <%= link.name %>
          </a>
        <% }) %>
      </div>
    <% } %>
    
    <!-- 社交图标(可选) -->
    <% 
      const social = theme_config('social', {});
      if (theme_config('footer.social', false) && Object.keys(social).length > 0) {
    %>
      <div class="footer-social">
        <% Object.keys(social).forEach(key => { %>
          <a 
            href="<%= social[key] %>" 
            class="social-icon"
            target="_blank"
            rel="noopener noreferrer"
            aria-label="<%= key %>"
          >
            <i class="icon-<%= key %>"></i>
          </a>
        <% }) %>
      </div>
    <% } %>
  </div>
</footer>

TOC 目录

themes/aurora/layout/_partial/toc.ejs

<%
  // 使用 cheerio 解析内容并生成目录
  const cheerio = require('cheerio');
  const $ = cheerio.load(content);
  
  const minDepth = theme_config('features.toc.min_depth', 2);
  const maxDepth = theme_config('features.toc.max_depth', 3);
  const showNumber = theme_config('features.toc.number', true);
  
  const selector = Array.from(
    {length: maxDepth - minDepth + 1}, 
    (_, i) => `h${minDepth + i}`
  ).join(', ');
  
  const headings = [];
  $(selector).each(function(index) {
    const $heading = $(this);
    const level = parseInt(this.name.substring(1));
    const text = $heading.text();
    const id = $heading.attr('id') || text.toLowerCase()
      .replace(/\s+/g, '-')
      .replace(/[^\w-]/g, '');
    
    headings.push({
      level: level,
      text: text,
      id: id
    });
  });
  
  if (headings.length === 0) {
    return '';
  }
%>

<div class="toc-widget">
  <div class="toc-header">
    <h3 class="toc-title">目录</h3>
    <button class="toc-toggle" aria-label="折叠目录">
      <i class="icon-chevron-up"></i>
    </button>
  </div>
  
  <nav class="toc-content">
    <ol class="toc-list">
      <%
        let currentLevel = minDepth;
        headings.forEach((heading, index) => {
          const nextHeading = headings[index + 1];
          const diff = heading.level - currentLevel;
          
          if (diff > 0) {
            // 打开新的嵌套层级
            for (let i = 0; i < diff; i++) {
              %>
              <ol class="toc-list toc-list-level-<%= heading.level %>">
              <%
            }
          } else if (diff < 0) {
            // 关闭嵌套层级
            %>
            </li>
            <%
            for (let i = 0; i < -diff; i++) {
              %>
              </ol>
              </li>
              <%
            }
          } else if (index > 0) {
            %>
            </li>
            <%
          }
          
          %>
          <li class="toc-item toc-level-<%= heading.level %>">
            <a 
              href="#<%= heading.id %>" 
              class="toc-link"
              data-target="<%= heading.id %>"
            >
              <% if (showNumber) { %>
                <span class="toc-number"></span>
              <% } %>
              <span class="toc-text"><%= heading.text %></span>
            </a>
          <%
          
          currentLevel = heading.level;
          
          if (!nextHeading) {
            %>
            </li>
            <%
            for (let i = 0; i < heading.level - minDepth + 1; i++) {
              %>
              </ol>
              <%
            }
          }
        });
      %>
    </ol>
  </nav>
</div>

Comments 评论系统

themes/aurora/layout/_partial/comments.ejs

<%
  const provider = theme_config('comments.provider', 'none');
  
  if (provider === 'none' || !theme_config('comments.enable', false)) {
    return '';
  }
%>

<div class="comments-wrapper" id="comments">
  <h3 class="comments-title">评论</h3>
  
  <% if (provider === 'gitalk') { %>
    <%- partial('_partial/comments/gitalk') %>
  <% } else if (provider === 'disqus') { %>
    <%- partial('_partial/comments/disqus') %>
  <% } else if (provider === 'valine') { %>
    <%- partial('_partial/comments/valine') %>
  <% } else if (provider === 'waline') { %>
    <%- partial('_partial/comments/waline') %>
  <% } %>
</div>

themes/aurora/layout/_partial/comments/gitalk.ejs

<div id="gitalk-container"></div>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
<script src="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js"></script>

<script>
  const gitalk = new Gitalk({
    clientID: '<%= theme_config('comments.gitalk.client_id') %>',
    clientSecret: '<%= theme_config('comments.gitalk.client_secret') %>',
    repo: '<%= theme_config('comments.gitalk.repo') %>',
    owner: '<%= theme_config('comments.gitalk.owner') %>',
    admin: <%- JSON.stringify(theme_config('comments.gitalk.admin', [])) %>,
    id: '<%= page._id || md5(page.path) %>',
    distractionFreeMode: false,
    language: '<%= config.language || 'zh-CN' %>'
  });
  
  gitalk.render('gitalk-container');
</script>

第四部分:样式系统

样式架构设计

采用 SCSS 进行样式组织,使用 BEM 命名规范。

themes/aurora/source/css/style.scss

// ============================================
// Aurora 主题样式入口
// ============================================

// 1. 变量和配置
@import 'variables';
@import 'mixins';

// 2. 基础样式
@import 'base/reset';
@import 'base/typography';
@import 'base/layout';

// 3. 组件样式
@import 'components/header';
@import 'components/footer';
@import 'components/sidebar';
@import 'components/article';
@import 'components/pagination';
@import 'components/toc';
@import 'components/comments';
@import 'components/widget';

// 4. 页面样式
@import 'pages/home';
@import 'pages/post';
@import 'pages/archive';
@import 'pages/page';

// 5. 工具类
@import 'utilities/spacing';
@import 'utilities/display';
@import 'utilities/text';

// 6. 主题模式
@import 'themes/light';
@import 'themes/dark';

// 7. 响应式
@import 'responsive';

变量定义

themes/aurora/source/css/_variables.scss

// ============================================
// 颜色系统
// ============================================

// 主色
$primary-color: #4dabf7 !default;
$primary-light: lighten($primary-color, 10%);
$primary-dark: darken($primary-color, 10%);

// 中性色
$white: #ffffff;
$black: #000000;
$gray-50: #f9fafb;
$gray-100: #f3f4f6;
$gray-200: #e5e7eb;
$gray-300: #d1d5db;
$gray-400: #9ca3af;
$gray-500: #6b7280;
$gray-600: #4b5563;
$gray-700: #374151;
$gray-800: #1f2937;
$gray-900: #111827;

// 语义色
$success-color: #10b981;
$warning-color: #f59e0b;
$error-color: #ef4444;
$info-color: #3b82f6;

// 浅色主题
$light-bg: $white;
$light-bg-secondary: $gray-50;
$light-text: $gray-900;
$light-text-secondary: $gray-600;
$light-border: $gray-200;

// 深色主题
$dark-bg: #1a1a1a;
$dark-bg-secondary: #242424;
$dark-text: #e0e0e0;
$dark-text-secondary: #a0a0a0;
$dark-border: #333333;

// ============================================
// 字体
// ============================================

$font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 
                   'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
$font-family-serif: Georgia, 'Times New Roman', serif;
$font-family-mono: 'Fira Code', 'Courier New', monospace;

$font-size-base: 16px;
$font-size-sm: 14px;
$font-size-lg: 18px;
$font-size-xl: 20px;

$line-height-base: 1.6;
$line-height-heading: 1.3;

// ============================================
// 间距
// ============================================

$spacing-unit: 8px;
$spacing-xs: $spacing-unit * 0.5;  // 4px
$spacing-sm: $spacing-unit;        // 8px
$spacing-md: $spacing-unit * 2;    // 16px
$spacing-lg: $spacing-unit * 3;    // 24px
$spacing-xl: $spacing-unit * 4;    // 32px
$spacing-2xl: $spacing-unit * 6;   // 48px
$spacing-3xl: $spacing-unit * 8;   // 64px

// ============================================
// 布局
// ============================================

$container-max-width: 1200px;
$content-max-width: 800px;
$sidebar-width: 300px;

$header-height: 64px;
$footer-height: 80px;

// ============================================
// 圆角
// ============================================

$border-radius-sm: 4px;
$border-radius-md: 8px;
$border-radius-lg: 12px;
$border-radius-xl: 16px;
$border-radius-full: 9999px;

// ============================================
// 阴影
// ============================================

$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);

// ============================================
// 过渡
// ============================================

$transition-fast: 150ms;
$transition-base: 250ms;
$transition-slow: 350ms;

$transition-timing: cubic-bezier(0.4, 0, 0.2, 1);

// ============================================
// 断点
// ============================================

$breakpoint-xs: 480px;
$breakpoint-sm: 640px;
$breakpoint-md: 768px;
$breakpoint-lg: 1024px;
$breakpoint-xl: 1280px;
$breakpoint-2xl: 1536px;

// ============================================
// Z-index
// ============================================

$z-dropdown: 1000;
$z-sticky: 1020;
$z-fixed: 1030;
$z-modal-backdrop: 1040;
$z-modal: 1050;
$z-popover: 1060;
$z-tooltip: 1070;

Mixins

themes/aurora/source/css/_mixins.scss

// ============================================
// 响应式断点
// ============================================

@mixin respond-to($breakpoint) {
  @if $breakpoint == 'xs' {
    @media (min-width: $breakpoint-xs) { @content; }
  } @else if $breakpoint == 'sm' {
    @media (min-width: $breakpoint-sm) { @content; }
  } @else if $breakpoint == 'md' {
    @media (min-width: $breakpoint-md) { @content; }
  } @else if $breakpoint == 'lg' {
    @media (min-width: $breakpoint-lg) { @content; }
  } @else if $breakpoint == 'xl' {
    @media (min-width: $breakpoint-xl) { @content; }
  } @else if $breakpoint == '2xl' {
    @media (min-width: $breakpoint-2xl) { @content; }
  }
}

// ============================================
// 截断文本
// ============================================

@mixin truncate($lines: 1) {
  @if $lines == 1 {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  } @else {
    display: -webkit-box;
    -webkit-line-clamp: $lines;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

// ============================================
// Flexbox 居中
// ============================================

@mixin flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

// ============================================
// 清除浮动
// ============================================

@mixin clearfix {
  &::after {
    content: '';
    display: table;
    clear: both;
  }
}

// ============================================
// 渐变背景
// ============================================

@mixin gradient-bg($angle, $start-color, $end-color) {
  background: linear-gradient($angle, $start-color, $end-color);
}

// ============================================
// 卡片样式
// ============================================

@mixin card($padding: $spacing-lg) {
  background-color: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: $border-radius-lg;
  padding: $padding;
  box-shadow: $shadow-sm;
  transition: box-shadow $transition-base $transition-timing;
  
  &:hover {
    box-shadow: $shadow-md;
  }
}

// ============================================
// 按钮重置
// ============================================

@mixin button-reset {
  appearance: none;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  outline: none;
  
  &:focus {
    outline: 2px solid var(--primary-color);
    outline-offset: 2px;
  }
}

// ============================================
// 滚动条样式
// ============================================

@mixin custom-scrollbar($width: 8px) {
  &::-webkit-scrollbar {
    width: $width;
    height: $width;
  }
  
  &::-webkit-scrollbar-track {
    background: var(--bg-secondary);
  }
  
  &::-webkit-scrollbar-thumb {
    background: var(--text-secondary);
    border-radius: $border-radius-full;
    
    &:hover {
      background: var(--text-primary);
    }
  }
}

基础样式 - Reset

themes/aurora/source/css/base/_reset.scss

// ============================================
// CSS Reset
// ============================================

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

html {
  font-size: $font-size-base;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

body {
  font-family: $font-family-sans;
  font-size: $font-size-base;
  line-height: $line-height-base;
  color: var(--text-primary);
  background-color: var(--bg-primary);
  transition: color $transition-base, background-color $transition-base;
}

// 标题
h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
  line-height: $line-height-heading;
  margin-bottom: $spacing-md;
}

// 链接
a {
  color: var(--primary-color);
  text-decoration: none;
  transition: color $transition-fast;
  
  &:hover {
    color: var(--primary-dark);
  }
}

// 图片
img {
  max-width: 100%;
  height: auto;
  display: block;
}

// 列表
ul, ol {
  list-style: none;
}

// 按钮
button {
  font-family: inherit;
  font-size: inherit;
}

// 表单元素
input,
textarea,
select {
  font-family: inherit;
  font-size: inherit;
}

// 代码
code,
pre {
  font-family: $font-family-mono;
}

组件样式 - Header

themes/aurora/source/css/components/_header.scss

// ============================================
// 顶部导航
// ============================================

.site-header {
  position: sticky;
  top: 0;
  left: 0;
  right: 0;
  height: $header-height;
  background-color: var(--bg-primary);
  border-bottom: 1px solid var(--border-color);
  backdrop-filter: blur(10px);
  z-index: $z-sticky;
  transition: all $transition-base;
  
  .header-container {
    max-width: $container-max-width;
    height: 100%;
    margin: 0 auto;
    padding: 0 $spacing-lg;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  
  // Logo 和站点标题
  .site-branding {
    display: flex;
    align-items: center;
    gap: $spacing-md;
    
    .site-logo-link {
      display: flex;
      align-items: center;
      gap: $spacing-sm;
      color: var(--text-primary);
      
      &:hover {
        color: var(--primary-color);
      }
    }
    
    .site-logo {
      height: 32px;
      width: auto;
    }
    
    .site-title {
      font-size: $font-size-xl;
      font-weight: 700;
    }
    
    .site-subtitle {
      display: none;
      
      @include respond-to('md') {
        display: block;
        font-size: $font-size-sm;
        color: var(--text-secondary);
      }
    }
  }
  
  // 导航菜单
  .site-nav {
    display: none;
    
    @include respond-to('md') {
      display: block;
    }
    
    .nav-menu {
      display: flex;
      gap: $spacing-md;
    }
    
    .nav-item {
      &.active .nav-link {
        color: var(--primary-color);
        font-weight: 600;
      }
    }
    
    .nav-link {
      display: flex;
      align-items: center;
      gap: $spacing-xs;
      padding: $spacing-sm $spacing-md;
      color: var(--text-primary);
      border-radius: $border-radius-md;
      transition: all $transition-fast;
      
      &:hover {
        color: var(--primary-color);
        background-color: var(--bg-secondary);
      }
    }
  }
  
  // 工具栏
  .header-tools {
    display: flex;
    align-items: center;
    gap: $spacing-sm;
  }
  
  .tool-button {
    @include button-reset;
    @include flex-center;
    width: 40px;
    height: 40px;
    color: var(--text-primary);
    border-radius: $border-radius-md;
    transition: all $transition-fast;
    
    &:hover {
      color: var(--primary-color);
      background-color: var(--bg-secondary);
    }
  }
  
  // 暗色模式切换
  .theme-toggle {
    .sun-icon,
    .moon-icon {
      display: none;
    }
    
    [data-theme="light"] & .sun-icon,
    [data-theme="dark"] & .moon-icon {
      display: block;
    }
  }
  
  // 汉堡菜单
  .menu-toggle {
    @include respond-to('md') {
      display: none;
    }
    
    .hamburger {
      display: flex;
      flex-direction: column;
      gap: 4px;
      
      span {
        display: block;
        width: 20px;
        height: 2px;
        background-color: currentColor;
        transition: transform $transition-base;
      }
    }
    
    &.active {
      .hamburger span:nth-child(1) {
        transform: translateY(6px) rotate(45deg);
      }
      .hamburger span:nth-child(2) {
        opacity: 0;
      }
      .hamburger span:nth-child(3) {
        transform: translateY(-6px) rotate(-45deg);
      }
    }
  }
  
  // 阅读进度条
  .reading-progress-bar {
    position: absolute;
    bottom: 0;
    left: 0;
    height: 3px;
    background: linear-gradient(
      90deg,
      var(--primary-color),
      var(--primary-light)
    );
    transition: width $transition-fast;
    width: 0;
  }
}

第五部分:JavaScript 脚本

主脚本文件

themes/aurora/source/js/main.js

/**
 * Aurora Theme - Main Script
 * ============================================
 */

(function() {
  'use strict';
  
  // ==========================================
  // 工具函数
  // ==========================================
  
  const utils = {
    // DOM 选择器
    $(selector, context = document) {
      return context.querySelector(selector);
    },
    
    $$(selector, context = document) {
      return Array.from(context.querySelectorAll(selector));
    },
    
    // 节流函数
    throttle(func, wait) {
      let timeout;
      return function(...args) {
        if (!timeout) {
          timeout = setTimeout(() => {
            timeout = null;
            func.apply(this, args);
          }, wait);
        }
      };
    },
    
    // 防抖函数
    debounce(func, wait) {
      let timeout;
      return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
      };
    },
    
    // 本地存储
    storage: {
      get(key, defaultValue = null) {
        try {
          const value = localStorage.getItem(key);
          return value !== null ? JSON.parse(value) : defaultValue;
        } catch (e) {
          return defaultValue;
        }
      },
      
      set(key, value) {
        try {
          localStorage.setItem(key, JSON.stringify(value));
          return true;
        } catch (e) {
          return false;
        }
      },
      
      remove(key) {
        try {
          localStorage.removeItem(key);
          return true;
        } catch (e) {
          return false;
        }
      }
    }
  };
  
  // ==========================================
  // 暗色模式切换
  // ==========================================
  
  const ThemeToggle = {
    init() {
      this.toggle = utils.$('#theme-toggle');
      if (!this.toggle) return;
      
      this.toggle.addEventListener('click', () => this.switch());
      
      // 监听系统主题变化
      if (window.matchMedia) {
        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
          if (utils.storage.get('theme') === 'auto') {
            this.apply(e.matches ? 'dark' : 'light');
          }
        });
      }
    },
    
    switch() {
      const current = document.documentElement.getAttribute('data-theme') || 'light';
      const next = current === 'light' ? 'dark' : 'light';
      this.apply(next);
      utils.storage.set('theme', next);
    },
    
    apply(theme) {
      document.documentElement.setAttribute('data-theme', theme);
    }
  };
  
  // ==========================================
  // 移动端菜单
  // ==========================================
  
  const MobileMenu = {
    init() {
      this.toggle = utils.$('#menu-toggle');
      this.nav = utils.$('#site-nav');
      if (!this.toggle || !this.nav) return;
      
      this.toggle.addEventListener('click', () => this.toggleMenu());
      
      // 点击菜单项后关闭菜单
      utils.$$('.nav-link', this.nav).forEach(link => {
        link.addEventListener('click', () => this.closeMenu());
      });
      
      // 点击外部关闭菜单
      document.addEventListener('click', (e) => {
        if (!this.toggle.contains(e.target) && !this.nav.contains(e.target)) {
          this.closeMenu();
        }
      });
    },
    
    toggleMenu() {
      const isOpen = this.nav.classList.toggle('active');
      this.toggle.classList.toggle('active', isOpen);
      document.body.style.overflow = isOpen ? 'hidden' : '';
    },
    
    closeMenu() {
      this.nav.classList.remove('active');
      this.toggle.classList.remove('active');
      document.body.style.overflow = '';
    }
  };
  
  // ==========================================
  // 阅读进度条
  // ==========================================
  
  const ReadingProgress = {
    init() {
      this.bar = utils.$('#reading-progress');
      if (!this.bar) return;
      
      window.addEventListener('scroll', utils.throttle(() => {
        this.update();
      }, 100));
    },
    
    update() {
      const winHeight = window.innerHeight;
      const docHeight = document.documentElement.scrollHeight;
      const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
      const progress = scrollTop / (docHeight - winHeight) * 100;
      
      this.bar.style.width = `${Math.min(progress, 100)}%`;
    }
  };
  
  // ==========================================
  // 返回顶部
  // ==========================================
  
  const BackToTop = {
    init() {
      this.button = utils.$('#back-to-top');
      if (!this.button) return;
      
      window.addEventListener('scroll', utils.throttle(() => {
        this.toggleVisibility();
      }, 200));
      
      this.button.addEventListener('click', () => this.scrollToTop());
    },
    
    toggleVisibility() {
      const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
      this.button.classList.toggle('visible', scrollTop > 300);
      
      // 更新滚动百分比
      const percent = utils.$('.back-to-top-percent', this.button);
      if (percent) {
        const docHeight = document.documentElement.scrollHeight;
        const winHeight = window.innerHeight;
        const progress = Math.round(scrollTop / (docHeight - winHeight) * 100);
        percent.textContent = `${progress}%`;
      }
    },
    
    scrollToTop() {
      window.scrollTo({
        top: 0,
        behavior: 'smooth'
      });
    }
  };
  
  // ==========================================
  // 目录导航
  // ==========================================
  
  const TOC = {
    init() {
      this.toc = utils.$('.toc-widget');
      if (!this.toc) return;
      
      this.links = utils.$$('.toc-link', this.toc);
      this.headings = this.links.map(link => {
        const target = link.getAttribute('data-target');
        return utils.$(`#${target}`);
      }).filter(Boolean);
      
      if (this.headings.length === 0) return;
      
      // 监听滚动
      window.addEventListener('scroll', utils.throttle(() => {
        this.updateActive();
      }, 100));
      
      // 点击平滑滚动
      this.links.forEach(link => {
        link.addEventListener('click', (e) => {
          e.preventDefault();
          const target = link.getAttribute('data-target');
          const heading = utils.$(`#${target}`);
          if (heading) {
            const top = heading.getBoundingClientRect().top + window.pageYOffset - 80;
            window.scrollTo({
              top: top,
              behavior: 'smooth'
            });
          }
        });
      });
      
      // 折叠/展开
      const toggle = utils.$('.toc-toggle', this.toc);
      if (toggle) {
        toggle.addEventListener('click', () => {
          this.toc.classList.toggle('collapsed');
        });
      }
    },
    
    updateActive() {
      const scrollTop = window.pageYOffset + 100;
      
      let activeIndex = -1;
      for (let i = 0; i < this.headings.length; i++) {
        const heading = this.headings[i];
        if (heading.offsetTop <= scrollTop) {
          activeIndex = i;
        } else {
          break;
        }
      }
      
      this.links.forEach((link, index) => {
        link.classList.toggle('active', index === activeIndex);
      });
    }
  };
  
  // ==========================================
  // 代码块复制
  // ==========================================
  
  const CodeCopy = {
    init() {
      utils.$$('.copy-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
          this.copy(e.target);
        });
      });
    },
    
    async copy(button) {
      const codeBlock = button.closest('.code-wrapper, .highlight');
      if (!codeBlock) return;
      
      const code = codeBlock.querySelector('code');
      if (!code) return;
      
      try {
        await navigator.clipboard.writeText(code.textContent);
        this.showSuccess(button);
      } catch (err) {
        this.showError(button);
      }
    },
    
    showSuccess(button) {
      const originalText = button.textContent;
      button.textContent = '已复制!';
      button.classList.add('success');
      
      setTimeout(() => {
        button.textContent = originalText;
        button.classList.remove('success');
      }, 2000);
    },
    
    showError(button) {
      const originalText = button.textContent;
      button.textContent = '复制失败';
      button.classList.add('error');
      
      setTimeout(() => {
        button.textContent = originalText;
        button.classList.remove('error');
      }, 2000);
    }
  };
  
  // ==========================================
  // 图片懒加载
  // ==========================================
  
  const LazyLoad = {
    init() {
      if ('IntersectionObserver' in window) {
        this.observer = new IntersectionObserver((entries) => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              this.loadImage(entry.target);
            }
          });
        });
        
        utils.$$('img[loading="lazy"], .lazyload').forEach(img => {
          this.observer.observe(img);
        });
      } else {
        // Fallback
        utils.$$('img[loading="lazy"], .lazyload').forEach(img => {
          this.loadImage(img);
        });
      }
    },
    
    loadImage(img) {
      const src = img.getAttribute('data-src') || img.getAttribute('src');
      if (src) {
        img.src = src;
        img.classList.remove('lazyload');
        img.classList.add('lazyloaded');
        
        if (this.observer) {
          this.observer.unobserve(img);
        }
      }
    }
  };
  
  // ==========================================
  // 页面加载动画
  // ==========================================
  
  const Loading = {
    init() {
      this.overlay = utils.$('#loading');
      if (!this.overlay) return;
      
      window.addEventListener('load', () => {
        setTimeout(() => {
          this.hide();
        }, 300);
      });
    },
    
    hide() {
      if (this.overlay) {
        this.overlay.style.opacity = '0';
        setTimeout(() => {
          this.overlay.style.display = 'none';
        }, 300);
      }
    }
  };
  
  // ==========================================
  // 初始化
  // ==========================================
  
  function init() {
    Loading.init();
    ThemeToggle.init();
    MobileMenu.init();
    ReadingProgress.init();
    BackToTop.init();
    TOC.init();
    CodeCopy.init();
    LazyLoad.init();
    
    console.log('Aurora Theme initialized');
  }
  
  // DOM 加载完成后初始化
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
  
})();

小结

本章我们完整地构建了一个 Hexo 主题 Aurora,学习了:

  1. 主题目录结构 - 标准的文件组织方式
  2. 配置系统 - 灵活且用户友好的配置文件
  3. Layout 布局 - 模板继承与布局设计
  4. Partial 模板 - 模块化组件开发
  5. 样式系统 - SCSS 架构与主题切换
  6. 脚本系统 - JavaScript 功能实现

关键要点

模块化设计 - 将功能拆分成独立的组件 ✅ 配置驱动 - 通过配置控制主题行为 ✅ 响应式优先 - 适配各种屏幕尺寸 ✅ 性能优化 - 懒加载、代码分割、资源优化 ✅ 用户体验 - 暗色模式、平滑动画、交互反馈 ✅ 可维护性 - 清晰的代码结构和命名规范


下期预告

Hexo 主题开发系列教程(四):高级功能与优化

将介绍:

  • 🔍 搜索功能实现
  • 💬 多评论系统集成
  • 📊 统计分析集成
  • ⚡ 性能优化技巧
  • 📱 PWA 支持
  • 🎨 高级自定义
  • 🚀 主题发布与维护