跳到主要内容

主题开发

主题是 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>&copy; {{ 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/

发布主题

准备工作

  1. 创建完整的 README.md
  2. 添加 LICENSE 文件
  3. 准备主题截图(static/images/screenshot.png)
  4. 准备主题预览图(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 站点到各种托管平台。