Hugo Partials 使用指南

什么是 Partials

Partials 是 Hugo 提供的一种模板复用机制,用于将可重复的模板片段提取出来,以便在多个页面或模板中复用。

  • Partials 文件存放路径:
      layouts/partials/
    
  • 文件可以是任意模板,如 head.htmlfooter.html 等。

基本使用方法

使用 partial 函数在模板中引入 partial:

      {{ partial "head.html" . }}
    
  • 第一个参数是 partial 文件名(相对于 layouts/partials)。
  • 第二个参数 . 是传入的数据上下文,通常是当前页面对象。

示例

layouts/partials/head.html

      <head>
    <meta charset="utf-8">
    <title>{{ .Title }}</title>
</head>
    

layouts/_default/baseof.html

      <!DOCTYPE html>
<html lang="zh-CN">
{{ partial "head.html" . }}
<body>
    {{ block "main" . }}{{ end }}
</body>
</html>
    

高级 Partials 技巧

带参数的 Partials

创建灵活的 partial,接受多种参数:

      <!-- filepath: layouts/partials/components/button.html -->
{{ $text := .text | default "点击这里" }}
{{ $url := .url | default "#" }}
{{ $class := .class | default "btn" }}
{{ $target := .target | default "_self" }}

<a href="{{ $url }}" 
   class="{{ $class }}" 
   target="{{ $target }}"
   {{ with .attrs }}{{ range $key, $value }}{{ $key }}="{{ $value }}"{{ end }}{{ end }}>
  {{ $text }}
</a>
    

使用示例:

      <!-- 基本使用 -->
{{ partial "components/button.html" (dict "text" "了解更多" "url" "/about/") }}

<!-- 高级使用 -->
{{ partial "components/button.html" (dict 
    "text" "下载PDF" 
    "url" "/downloads/guide.pdf" 
    "class" "btn btn-download" 
    "target" "_blank"
    "attrs" (dict "download" "" "data-analytics" "download-guide")
) }}
    

条件渲染 Partials

      <!-- filepath: layouts/partials/content/social-share.html -->
{{ if .Site.Params.enableSocialShare }}
  <div class="social-share">
    <h4>分享这篇文章</h4>
    {{ if .Site.Params.social.twitter }}
      <a href="https://twitter.com/intent/tweet?url={{ .Permalink }}&text={{ .Title }}" 
         target="_blank" rel="noopener">
        分享到 Twitter
      </a>
    {{ end }}
    
    {{ if .Site.Params.social.facebook }}
      <a href="https://www.facebook.com/sharer/sharer.php?u={{ .Permalink }}" 
         target="_blank" rel="noopener">
        分享到 Facebook
      </a>
    {{ end }}
  </div>
{{ end }}
    

循环生成内容

      <!-- filepath: layouts/partials/content/post-list.html -->
<div class="post-list">
  {{ range . }}
    <article class="post-card">
      {{ if .Params.featured_image }}
        <img src="{{ .Params.featured_image }}" alt="{{ .Title }}" class="post-image">
      {{ end }}
      
      <div class="post-content">
        <h3><a href="{{ .Permalink }}">{{ .Title }}</a></h3>
        <p class="post-meta">
          <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2006年1月2日" }}</time>
          {{ with .Params.author }}
            <span class="author">作者:{{ . }}</span>
          {{ end }}
        </p>
        <p class="post-summary">{{ .Summary | truncate 120 }}</p>
        <a href="{{ .Permalink }}" class="read-more">阅读更多 →</a>
      </div>
    </article>
  {{ end }}
</div>
    

Partial 缓存优化

使用 partialCached

对于计算开销较大的 partial,使用 partialCached 提升性能:

      <!-- 缓存站点范围的 partial -->
{{ partialCached "expensive-computation.html" . }}

<!-- 基于页面缓存 -->
{{ partialCached "post-related.html" . .RelPermalink }}

<!-- 基于多个变量缓存 -->
{{ partialCached "paginated-list.html" . .Section .Paginator.PageNumber }}
    

缓存键策略

      <!-- 按语言缓存 -->
{{ partialCached "navigation.html" . .Site.Language.Lang }}

<!-- 按用户类型缓存 -->
{{ $userType := .Params.userType | default "anonymous" }}
{{ partialCached "personalized-content.html" . $userType }}

<!-- 组合缓存键 -->
{{ $cacheKey := printf "%s-%s-%d" .Section .Site.Language.Lang .Paginator.PageNumber }}
{{ partialCached "section-list.html" . $cacheKey }}
    

实际应用案例

响应式导航菜单

      <!-- filepath: layouts/_partials/site/navigation.html -->
<nav class="main-navigation" id="main-nav">
  <div class="nav-container">
    <!-- 网站标志 -->
    <div class="nav-brand">
      <a href="{{ .Site.BaseURL }}">
        {{ if .Site.Params.logo }}
          <img src="{{ .Site.Params.logo }}" alt="{{ .Site.Title }}" class="logo">
        {{ else }}
          <span class="site-title">{{ .Site.Title }}</span>
        {{ end }}
      </a>
    </div>
    
    <!-- 移动端菜单按钮 -->
    <button class="nav-toggle" id="nav-toggle" aria-label="切换导航菜单">
      <span></span>
      <span></span>
      <span></span>
    </button>
    
    <!-- 导航菜单 -->
    <div class="nav-menu" id="nav-menu">
      {{ $currentPage := . }}
      {{ range .Site.Menus.main }}
        {{ $isActive := or ($currentPage.IsMenuCurrent "main" .) ($currentPage.HasMenuCurrent "main" .) }}
        <a href="{{ .URL }}" 
           class="nav-link{{ if $isActive }} active{{ end }}">
          {{ .Name }}
        </a>
      {{ end }}
      
      <!-- 搜索框 -->
      {{ if .Site.Params.enableSearch }}
        <div class="nav-search">
          <input type="search" placeholder="搜索..." class="search-input">
          <button type="submit" class="search-submit">🔍</button>
        </div>
      {{ end }}
    </div>
  </div>
</nav>
    

文章卡片组件

      <!-- filepath: layouts/_partials/content/article-card.html -->
{{ $page := . }}
{{ $showExcerpt := .showExcerpt | default true }}
{{ $showMeta := .showMeta | default true }}

<article class="article-card">
  <!-- 特色图片 -->
  {{ with $page.Params.featured_image }}
    <div class="card-image">
      <a href="{{ $page.Permalink }}">
        <img src="{{ . }}" alt="{{ $page.Title }}" loading="lazy">
      </a>
      
      {{ if $page.Params.featured }}
        <span class="featured-badge">推荐</span>
      {{ end }}
    </div>
  {{ end }}
  
  <div class="card-content">
    <!-- 分类标签 -->
    {{ with $page.Params.categories }}
      <div class="card-categories">
        {{ range first 2 . }}
          <span class="category">{{ . }}</span>
        {{ end }}
      </div>
    {{ end }}
    
    <!-- 标题 -->
    <h3 class="card-title">
      <a href="{{ $page.Permalink }}">{{ $page.Title }}</a>
    </h3>
    
    <!-- 元信息 -->
    {{ if $showMeta }}
      <div class="card-meta">
        <time datetime="{{ $page.Date.Format "2006-01-02" }}">
          {{ $page.Date.Format "2006年1月2日" }}
        </time>
        
        {{ with $page.Params.author }}
          <span class="author">{{ . }}</span>
        {{ end }}
        
        <span class="reading-time">{{ $page.ReadingTime }} 分钟阅读</span>
      </div>
    {{ end }}
    
    <!-- 摘要 -->
    {{ if $showExcerpt }}
      <p class="card-excerpt">
        {{ $page.Summary | truncate 150 "..." }}
      </p>
    {{ end }}
    
    <!-- 标签 -->
    {{ with $page.Params.tags }}
      <div class="card-tags">
        {{ range first 3 . }}
          <span class="tag">#{{ . }}</span>
        {{ end }}
      </div>
    {{ end }}
    
    <!-- 阅读链接 -->
    <a href="{{ $page.Permalink }}" class="read-more">
      阅读全文 →
    </a>
  </div>
</article>
    

SEO 优化组件

      <!-- filepath: layouts/_partials/site/seo.html -->
<!-- 基本 meta 标签 -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
<meta name="keywords" content="{{ delimit (.Keywords | default .Site.Params.keywords) ", " }}">
<meta name="author" content="{{ .Params.author | default .Site.Params.author }}">

<!-- 页面标题 -->
<title>
  {{ if .IsHome }}
    {{ .Site.Title }}{{ with .Site.Params.tagline }} - {{ . }}{{ end }}
  {{ else }}
    {{ .Title }} - {{ .Site.Title }}
  {{ end }}
</title>

<!-- Open Graph -->
<meta property="og:title" content="{{ .Title }}">
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
<meta property="og:url" content="{{ .Permalink }}">
{{ with .Params.featured_image }}
<meta property="og:image" content="{{ . | absURL }}">
{{ end }}

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
{{ with .Site.Params.social.twitter }}
<meta name="twitter:site" content="@{{ . }}">
{{ end }}

<!-- JSON-LD 结构化数据 -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "{{ if .IsPage }}Article{{ else }}WebSite{{ end }}",
  "name": "{{ .Title }}",
  "description": "{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}",
  "url": "{{ .Permalink }}"
  {{ if .IsPage }}
  ,"author": {
    "@type": "Person",
    "name": "{{ .Params.author | default .Site.Params.author }}"
  },
  "datePublished": "{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}",
  "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}"
  {{ end }}
}
</script>
    

错误处理与调试

安全的 Partial 调用

      <!-- 检查 partial 是否存在 -->
{{ if templates.Exists "partials/optional-component.html" }}
  {{ partial "optional-component.html" . }}
{{ end }}

<!-- 防御性编程 -->
{{ with .Params.customData }}
  {{ partial "custom-component.html" . }}
{{ else }}
  {{ partial "default-component.html" . }}
{{ end }}
    

调试 Partials

      <!-- 添加调试信息 -->
{{ if not hugo.IsProduction }}
  <!-- 开始 Partial: header.html -->
{{ end }}

{{ partial "site/header.html" . }}

{{ if not hugo.IsProduction }}
  <!-- 结束 Partial: header.html -->
{{ end }}
    

性能优化建议

合理使用缓存

      <!-- 适合缓存的场景 -->
{{ partialCached "expensive-analytics.html" . }}

<!-- 不适合缓存的场景(经常变化的内容) -->
{{ partial "current-time.html" . }}
    

避免过度嵌套

      <!-- 避免过深的 partial 嵌套 -->
{{ partial "level1.html" . }}
  <!-- level1.html 中调用 level2.html -->
    <!-- level2.html 中调用 level3.html -->
      <!-- 应该避免更深的嵌套 -->
    

优化数据传递

      <!-- 只传递必要的数据 -->
{{ $pageData := dict "title" .Title "url" .Permalink "date" .Date }}
{{ partial "simple-card.html" $pageData }}

<!-- 而不是传递整个页面对象 -->
{{ partial "simple-card.html" . }}
    

最佳实践

命名规范

  • 使用描述性名称:article-card.html 而不是 card.html
  • 采用连字符分隔:social-links.html
  • 按功能分类:content/, site/, components/

文档化

      <!-- filepath: layouts/partials/components/button.html -->
{{/*
  按钮组件
  
  参数:
  - text (string): 按钮文本
  - url (string): 链接地址
  - class (string): CSS 类名
  - target (string): 链接目标,默认 "_self"
  
  示例:
  {{ partial "components/button.html" (dict "text" "点击我" "url" "/page/") }}
*/}}

{{ $text := .text | default "按钮" }}
{{ $url := .url | default "#" }}
{{ $class := .class | default "btn" }}
{{ $target := .target | default "_self" }}

<a href="{{ $url }}" 
   class="{{ $class }}" 
   target="{{ $target }}"
   {{ with .attrs }}{{ range $key, $value }}{{ $key }}="{{ $value }}"{{ end }}{{ end }}>
  {{ $text }}
</a>
    

动态引入 Partials

Hugo 允许使用 printf 或其他字符串操作函数动态拼接 partial 路径:

      {{ $pathName := "components/header" }}
{{ partial (printf "%s.html" $pathName) . }}
    

示例

假设我们有多个 header partial:

      layouts/partials/components/header-home.html
layouts/partials/components/header-about.html
    

根据页面类型动态加载:

      <!-- .Type根据 内容文件所在的目录来自动确定的 -->
<!-- 例如content/posts/hello.md,.Type 的值就是 "posts" -->
{{ $pageType := .Type }}
{{ partial (printf "components/header-%s.html" $pageType) . }}
    

使用 Scratch 临时存储路径

      {{ $.Scratch.Set "pathName" "components/header" }}
{{ partial (printf "%s.html" ($.Scratch.Get "pathName")) . }}
    
  • $.Scratch.Set:设置临时变量。
  • $.Scratch.Get:获取变量。
  • $ 表示全局上下文。

示例项目结构

      my-hugo-site/
├── layouts/
│   ├── _default/
│   │   └── baseof.html
│   └── partials/
│       ├── head.html
│       ├── footer.html
│       ├── alert.html
│       └── components/
│           ├── header-home.html
│           └── header-about.html
├── content/
│   ├── _index.md
│   ├── home.md
│   └── about.md
    

内容示例

content/home.md

      ---
title: "首页"
type: "home"
---
欢迎来到首页。
    

content/about.md

      ---
title: "关于"
type: "about"
---
这里是关于页面。
    

baseof.html 动态加载 header

      <!DOCTYPE html>
<html lang="zh-CN">
{{ partial "head.html" . }}
<body>
    {{ $pageType := .Type }}
    {{ partial (printf "components/header-%s.html" $pageType) . }}
    {{ block "main" . }}{{ end }}
    {{ partial "footer.html" . }}
</body>
</html>
    

注意事项

  1. 路径相对性:partial 路径是相对于 layouts/partials 的。
  2. 传参:传入 partial 的上下文可以是 .dict
  3. 动态路径:要确保动态生成的 partial 路径存在,否则渲染会报错。
  4. 缓存:Hugo 会缓存 partial 输出,提高性能。
  5. 单一职责:每个 partial 只负责一个功能
  6. 避免复杂逻辑:将复杂处理移到调用方
  7. 合理参数:参数不宜过多

声明

作者: liyao

版权:本博客所有文章除特别声明外,均采用CCBY-NC-SA4.O许可协议。转载请注明!

最后更新于 2025-10-02 16:28 history