主题开发
主题是 Hugo 站点外观和功能的核心。本章将介绍如何开发和使用 Hugo 主题。
主题基础
主题目录结构
一个完整的 Hugo 主题具有以下结构:
themes/
└── my-theme/
├── archetypes/
│ └── default.md
├── assets/
│ ├── css/
│ │ └── main.css
│ └── js/
│ └── main.js
├── layouts/
│ ├── _default/
│ │ ├── baseof.html
│ │ ├── list.html
│ │ └── single.html
│ ├── partials/
│ │ ├── header.html
│ │ ├── footer.html
│ │ └── nav.html
│ ├── shortcodes/
│ │ └── alert.html
│ ├── 404.html
│ └── index.html
├── static/
│ ├── images/
│ └── fonts/
├── theme.toml
└── README.md
theme.toml 配置
theme.toml 是主题的元数据文件:
name = "My Theme"
license = "MIT"
licenselink = "https://github.com/user/my-theme/blob/main/LICENSE"
description = "A simple Hugo theme"
homepage = "https://github.com/user/my-theme"
tags = ["blog", "minimal", "responsive"]
features = ["blog", "syntax-highlighting"]
min_version = "0.92.0"
[author]
name = "Your Name"
homepage = "https://yourwebsite.com"
[params]
dateFormat = "2006-01-02"
showReadingTime = true
创建基础模板
baseof.html
基础模板定义页面的骨架结构:
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
<!-- SEO Meta Tags -->
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
<meta name="keywords" content="{{ with .Keywords }}{{ delimit . ", " }}{{ end }}">
<meta name="author" content="{{ .Site.Params.author }}">
<!-- Open Graph -->
{{ template "_internal/opengraph.html" . }}
<!-- Twitter Card -->
{{ template "_internal/twitter_cards.html" . }}
<!-- Favicon -->
<link rel="icon" href="{{ "favicon.ico" | relURL }}">
<!-- CSS -->
{{ $css := resources.Get "css/main.css" | minify | fingerprint }}
<link rel="stylesheet" href="{{ $css.Permalink }}">
{{ block "head" . }}{{ end }}
</head>
<body>
{{ partial "header.html" . }}
<main>
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
<!-- JavaScript -->
{{ $js := resources.Get "js/main.js" | minify | fingerprint }}
<script src="{{ $js.Permalink }}" defer></script>
{{ block "scripts" . }}{{ end }}
</body>
</html>
首页模板
layouts/index.html:
{{ define "main" }}
<section class="hero">
<div class="container">
<h1>{{ .Site.Title }}</h1>
<p>{{ .Site.Params.description }}</p>
<a href="/posts/" class="btn">浏览文章</a>
</div>
</section>
<section class="featured-posts">
<div class="container">
<h2>精选文章</h2>
<div class="posts-grid">
{{ range first 6 (where .Site.RegularPages "Section" "posts") }}
{{ partial "post-card.html" . }}
{{ end }}
</div>
</div>
</section>
<section class="about">
<div class="container">
{{ .Content }}
</div>
</section>
{{ end }}
单页模板
layouts/_default/single.html:
{{ define "head" }}
{{ with .Params.image }}
<meta property="og:image" content="{{ . | absURL }}">
{{ end }}
{{ end }}
{{ define "main" }}
<article class="post">
<header class="post-header">
{{ with .Params.image }}
<div class="post-cover">
<img src="{{ . }}" alt="{{ $.Title }}">
</div>
{{ end }}
<h1 class="post-title">{{ .Title }}</h1>
<div class="post-meta">
<time datetime="{{ .Date.Format "2006-01-02" }}">
{{ .Date.Format "2006年01月02日" }}
</time>
{{ if .Site.Params.showReadingTime }}
<span class="reading-time">
{{ .ReadingTime }} 分钟阅读
</span>
{{ end }}
{{ with .Params.author }}
<span class="author">{{ . }}</span>
{{ end }}
</div>
{{ with .Params.tags }}
<div class="post-tags">
{{ range . }}
<a href="{{ "tags/" | relURL }}{{ . | urlize }}/" class="tag">{{ . }}</a>
{{ end }}
</div>
{{ end }}
</header>
{{ if .Params.toc | default .Site.Params.showToc }}
<aside class="toc">
<h3>目录</h3>
{{ .TableOfContents }}
</aside>
{{ end }}
<div class="post-content">
{{ .Content }}
</div>
<footer class="post-footer">
{{ partial "post-nav.html" . }}
</footer>
</article>
{{ partial "related-posts.html" . }}
{{ end }}
列表模板
layouts/_default/list.html:
{{ define "main" }}
<div class="list-header">
<h1>{{ .Title }}</h1>
{{ with .Description }}
<p class="description">{{ . }}</p>
{{ end }}
</div>
{{ .Content }}
<div class="posts-list">
{{ range .Paginator.Pages }}
{{ partial "post-card.html" . }}
{{ end }}
</div>
{{ template "_internal/pagination.html" . }}
{{ end }}
Partials 组件
头部组件
layouts/partials/header.html:
<header class="site-header">
<div class="container">
<a href="{{ .Site.BaseURL }}" class="logo">
{{ with .Site.Params.logo }}
<img src="{{ . }}" alt="{{ $.Site.Title }}">
{{ else }}
<span>{{ .Site.Title }}</span>
{{ end }}
</a>
{{ partial "nav.html" . }}
<button class="menu-toggle" aria-label="切换菜单">
<span></span>
<span></span>
<span></span>
</button>
</div>
</header>
导航组件
layouts/partials/nav.html:
<nav class="site-nav">
<ul>
{{ range .Site.Menus.main }}
<li class="{{ if $.IsMenuCurrent "main" . }}active{{ end }}">
<a href="{{ .URL }}">{{ .Name }}</a>
{{ if .HasChildren }}
<ul class="submenu">
{{ range .Children }}
<li><a href="{{ .URL }}">{{ .Name }}</a></li>
{{ end }}
</ul>
{{ end }}
</li>
{{ end }}
</ul>
</nav>
文章卡片组件
layouts/partials/post-card.html:
<article class="post-card">
{{ with .Params.image }}
<a href="{{ $.RelPermalink }}" class="post-card-image">
<img src="{{ . }}" alt="{{ $.Title }}">
</a>
{{ end }}
<div class="post-card-content">
<div class="post-card-meta">
<time datetime="{{ .Date.Format "2006-01-02" }}">
{{ .Date.Format "2006-01-02" }}
</time>
{{ with .Params.categories }}
<span class="category">{{ index . 0 }}</span>
{{ end }}
</div>
<h2 class="post-card-title">
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
</h2>
<p class="post-card-excerpt">
{{ .Summary | plainify | truncate 150 }}
</p>
{{ with .Params.tags }}
<div class="post-card-tags">
{{ range first 3 . }}
<span class="tag">{{ . }}</span>
{{ end }}
</div>
{{ end }}
</div>
</article>
页脚组件
layouts/partials/footer.html:
<footer class="site-footer">
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h3>{{ .Site.Title }}</h3>
<p>{{ .Site.Params.description }}</p>
</div>
<div class="footer-section">
<h3>快速链接</h3>
<ul>
{{ range .Site.Menus.footer }}
<li><a href="{{ .URL }}">{{ .Name }}</a></li>
{{ end }}
</ul>
</div>
<div class="footer-section">
<h3>社交媒体</h3>
<div class="social-links">
{{ with .Site.Params.github }}
<a href="{{ . }}" target="_blank" rel="noopener">GitHub</a>
{{ end }}
{{ with .Site.Params.twitter }}
<a href="{{ . }}" target="_blank" rel="noopener">Twitter</a>
{{ end }}
</div>
</div>
</div>
<div class="footer-bottom">
<p>© {{ now.Year }} {{ .Site.Title }}. 保留所有权利。</p>
<p>由 <a href="https://gohugo.io" target="_blank" rel="noopener">Hugo</a> 驱动</p>
</div>
</div>
</footer>
相关文章组件
layouts/partials/related-posts.html:
{{ $related := .Site.RegularPages.Related . | first 3 }}
{{ with $related }}
<aside class="related-posts">
<h2>相关文章</h2>
<div class="related-grid">
{{ range . }}
<article class="related-item">
{{ with .Params.image }}
<img src="{{ . }}" alt="{{ $.Title }}">
{{ end }}
<h3><a href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
<time>{{ .Date.Format "2006-01-02" }}</time>
</article>
{{ end }}
</div>
</aside>
{{ end }}
文章导航组件
layouts/partials/post-nav.html:
<nav class="post-nav">
{{ with .PrevInSection }}
<a href="{{ .RelPermalink }}" class="prev">
<span class="label">上一篇</span>
<span class="title">{{ .Title }}</span>
</a>
{{ end }}
{{ with .NextInSection }}
<a href="{{ .RelPermalink }}" class="next">
<span class="label">下一篇</span>
<span class="title">{{ .Title }}</span>
</a>
{{ end }}
</nav>
CSS 组织
使用 Assets 管理样式
assets/css/main.css:
/* 变量定义 */
:root {
--color-primary: #3b82f6;
--color-text: #1f2937;
--color-bg: #ffffff;
--color-border: #e5e7eb;
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "SF Mono", Monaco, "Cascadia Code", monospace;
--spacing-unit: 1rem;
--max-width: 1200px;
}
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
color: var(--color-text);
background-color: var(--color-bg);
line-height: 1.6;
}
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 var(--spacing-unit);
}
/* 头部样式 */
.site-header {
padding: 1.5rem 0;
border-bottom: 1px solid var(--color-border);
}
/* 文章卡片样式 */
.post-card {
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.2s;
}
.post-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 0 1rem;
}
.posts-grid {
grid-template-columns: 1fr;
}
}
使用 Sass/SCSS
assets/css/main.scss:
// 变量
$primary-color: #3b82f6;
$text-color: #1f2937;
$bg-color: #ffffff;
$border-color: #e5e7eb;
// 混入
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin card-shadow {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
// 样式
.post-card {
border: 1px solid $border-color;
border-radius: 8px;
&:hover {
@include card-shadow;
}
&-title {
color: $text-color;
font-size: 1.25rem;
}
}
在模板中引用:
{{ $scss := resources.Get "css/main.scss" | toCSS | minify | fingerprint }}
<link rel="stylesheet" href="{{ $scss.Permalink }}">
JavaScript 组织
assets/js/main.js:
// 移动端菜单
document.addEventListener('DOMContentLoaded', function() {
const menuToggle = document.querySelector('.menu-toggle');
const nav = document.querySelector('.site-nav');
if (menuToggle && nav) {
menuToggle.addEventListener('click', function() {
nav.classList.toggle('active');
menuToggle.classList.toggle('active');
});
}
});
// 代码高亮行号
document.querySelectorAll('pre code').forEach((block) => {
// 添加复制按钮
const button = document.createElement('button');
button.className = 'copy-button';
button.textContent = '复制';
button.addEventListener('click', () => {
navigator.clipboard.writeText(block.textContent);
button.textContent = '已复制';
setTimeout(() => button.textContent = '复制', 2000);
});
block.parentNode.appendChild(button);
});
主题配置
定义主题参数
在 theme.toml 中定义默认参数:
[params]
dateFormat = "2006-01-02"
showReadingTime = true
showToc = true
showShareButtons = true
mainSections = ["posts"]
[params.hero]
enable = true
title = "欢迎来到我的博客"
subtitle = "分享技术与生活"
[params.footer]
copyright = "保留所有权利"
poweredBy = true
在站点配置中覆盖
用户可以在站点的 hugo.toml 中覆盖主题参数:
[params]
dateFormat = "2006年01月02日"
showReadingTime = false
[params.hero]
title = "我的技术博客"
主题继承
用户可以在不修改主题的情况下覆盖模板:
my-site/
├── layouts/
│ ├── _default/
│ │ └── single.html # 覆盖主题的单页模板
│ └── partials/
│ └── header.html # 覆盖主题的头部组件
└── themes/
└── my-theme/
发布主题
准备工作
- 创建完整的 README.md
- 添加 LICENSE 文件
- 准备主题截图(static/images/screenshot.png)
- 准备主题预览图(static/images/tn.png)
README 模板
# My Theme
一个简洁优雅的 Hugo 博客主题。
## 特性
- 响应式设计
- 深色模式支持
- SEO 优化
- 代码高亮
- 文章目录
## 安装
```bash
cd your-hugo-site
git submodule add https://github.com/user/my-theme.git themes/my-theme
配置
在 hugo.toml 中添加:
theme = 'my-theme'
参数配置
[params]
dateFormat = "2006-01-02"
showReadingTime = true
许可证
MIT License
### 提交到主题库
1. Fork [hugoThemes](https://github.com/gohugoio/hugoThemes) 仓库
2. 在 `themes.txt` 中添加你的主题
3. 提交 Pull Request
## 小结
本章介绍了 Hugo 主题开发的核心知识:
1. 主题目录结构和配置文件
2. 基础模板和页面模板的创建
3. Partials 组件的复用
4. CSS 和 JavaScript 的组织
5. 主题参数的定义和覆盖
6. 主题发布流程
下一章将学习如何部署 Hugo 站点到各种托管平台。