HUGO - темизация

26-12-2022

Предыдущая часть

Темизация Hugo

Для Hugo создано очень много тем. Впрочем не трудно создать собственную:

hugo new theme mytheme

И прописать в файл, config/_default/config.toml. Либо использовать команды:

hugo server -t themename
hugo -t themename

Если использовать тему, клонированную из репозитория git, не стоит редактировать файлы темы напрямую. Вместо этого настройка темы в Hugo - это вопрос переопределения шаблонов, доступных в теме. Это обеспечивает дополнительную гибкость настройки темы в соответствии с потребностями, оставаясь при этом в курсе исходной темы. Рабочий каталог проекта имеет приоритет, перед каталогами темы и файлы и шаблоны в нем перепишут такие-же в теме.

Компоненты темы

mytheme/
├── archetypes
│   └── default.md
├── layouts
│   ├── 404.html
│   ├── _default
│   │   ├── baseof.html
│   │   ├── list.html
│   │   └── single.html
│   ├── index.html
│   └── partials
│       ├── footer.html
│       ├── header.html
│       └── head.html
├── LICENSE
├── static
│   ├── css
│   ├── js
└── theme.toml

theme.toml - файл с настройками авторства и т.д.
archetypes - имеет такое-же значение как и в основе сайта, но меньший приоритет.
static - аналогично каталогу в корне проекта сайта.
layouts - тоже самое что и в корне сайта.

Желательно заполнить файлы theme.toml и LICENSE. Это необязательно, но если распространять свою тему, там сообщается миру, кого хвалить (или винить). Также неплохо объявить лицензию, чтобы люди знали, как они могут использовать тему.

Статические файлы

В папку mytheme/static сохраним необходимые файлы изображений, CSS и JS. Используем популярный фреймворк bootstrap. В директории проекта используем команду:

npm install bootstrap bootstrap-icons

Из каталога node_modules/bootstrap/dist/ скопируем нужные CSS и JS файлы в mytheme/static и добавим собственный файл logo.svg. Также скопируем из node_modules/bootstrap-icons/font  каталог fonts и bootstrap-icons.css.

Шаблоны

В Hugo есть несколько основных типа шаблонов. Это шаблон домашней страницы - mytheme/layouts/index.html. Используется только главной страницей. Также есть "одиночные" шаблоны, которые используются для создания выходных данных для одного файла содержимого - mytheme/layouts/_default/single.html. А также шаблоны "списков", которые используются для группировки нескольких частей контента перед созданием вывода - mytheme/layouts/_default/list.html.

Существует еще три типа шаблонов: частичные - mytheme/layouts/partials, представления содержимого - summary.html и термины - terms.html.

Макеты, блоки и части

Изменим самый главный базовый шаблон в themes/mytheme/layouts/_default/baseof.html

<!doctype html>
<html lang="ru" class="h-100">
{{- partial "head.html" . -}}

<body class="d-flex flex-column h-100">
    <i class="bi bi-caret-up-fill bg-secondary text-white" onclick="topFunction()" id="btnTop" title="Вверх"></i>
    {{- partial "header.html" . -}}
    <main class="flex-shrink-0 m-4">
        <div class="container">
            <div id="content">
                {{- block "main" . }}{{- end }}
            </div>
        </div>
    </main>
    {{- partial "footer.html" . -}}
    {{- partial "script.html" . -}}
    {{ if (ne .IsHome true) }}
    {{- partial "btntop.html" . -}}
    {{ end -}}
</body>

</html>

Это базовый макет по умолчанию для всего на сайте. Можно определить макеты для разных типов контента, это запасной вариант, когда ничего другого не указано. Внутри этого мы определяем несколько partials и blocks.

{{- partial "head.html" . -}}

Эта строчка означает, что hugo вставит сюда содержимое из файла partials/head.html. Очень похоже на технологию - SSI.

{{- block "main" . }}{{- end }}

Эта строка добавит содержимое из файлов - mytheme/layouts/_default/single.html или mytheme/layouts/_default/list.html, в которых определен свой вывод блока - main.

head.html

Внутри themes/mytheme/layouts/partials/head.html добавим bootstrap.

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="robots" content="index, follow" />
    {{ $keywords := .Site.Params.keywords -}}
    {{ if .Params.tags }}{{ $keywords = .Params.tags }}{{ end -}}
    <meta name="keywords" content="{{ delimit $keywords " , " }}" />
    {{ $description := .Site.Params.description -}}
    {{ if .Params.description }}{{ $description = .Params.description }}{{ end -}}
    <meta name="description" content="{{ $description }}">
    {{ $author := .Site.Author.name -}}
    {{ if .Params.Author }}{{ $author = .Params.Author }}{{ end -}}
    <meta name="author" content="{{ $author }}">
    {{ template "_internal/opengraph.html" . }}
    <link rel="canonical" href="{{ .Permalink }}">
    <link rel="shortlink" href="{{ .Site.BaseURL }}" />
    <link rel="image_src" href="{{ .Site.BaseURL }}images/logo.svg" />
    {{ $title := print .Title " | " .Site.Title -}}
    {{ if .IsHome }}{{ $title = .Site.Title }}{{ end -}}
    <title>{{ $title }}</title>
    <link rel="stylesheet" type="text/css" href="{{ .Site.BaseURL }}css/bootstrap.min.css">
    <link rel="stylesheet" type="text/css" href="{{ .Site.BaseURL }}css/bootstrap-icons.css">
    <link rel="stylesheet" type="text/css" href="{{ .Site.BaseURL }}css/style.css">
    {{ partial "favicon.html" . }}
    {{ with .OutputFormats.Get "rss" -}}{{ printf `
    <link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}{{ end
    }}
    {{- partial "copypasta.html" . -}}
</head>

Это настраивает bootstrap для загрузки из каталога static/css, а также добавляет ссылку на сгенерированный RSS-канал. Определяем метатеги keywords, description и author, изначально берем значения из файла конфигурации config/_default/params.toml, если в заголовке текущей страницы они определены, то используем их. Те же действия производим с заголовком страницы - title.

{{ template "_internal/opengraph.html" . }}

Эта строчка вставляет внутренний шаблон Hugo.

Внутренние шаблоны:

  • _internal/disqus.html
  • _internal/google_analytics.html
  • _internal/google_analytics_async.html
  • _internal/opengraph.html
  • _internal/pagination.html
  • _internal/schema.html
  • _internal/twitter_cards.html

favicon.html

Отдельно сформируем ссылки на иконки для разных устройств.

<link rel="apple-touch-icon" sizes="180x180" href="{{ .Site.BaseURL }}apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ .Site.BaseURL }}favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ .Site.BaseURL }}favicon-16x16.png">
<link rel="icon" type="image/vnd.microsoft.icon" href="{{ .Site.BaseURL }}favicon.ico">
<link rel="manifest" href="{{ .Site.BaseURL }}site.webmanifest">
<link rel="mask-icon" href="{{ .Site.BaseURL }}safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">

copypasta.html

Небольшой скрипт, который добавит ссылку на источник, при копировании с сайта.

<script type="text/javascript">
    document.oncopy = function(){
        var body = document.getElementsByTagName('body')[0];
        var selection = window.getSelection();
        var div = document.createElement('div');

        div.style.position = 'absolute';
        div.style.left = '-99999px';
        body.appendChild(div);
        div.innerHTML = selection + ' | Источник: ' + window.location.href;
        selection.selectAllChildren(div);

        window.setTimeout(function(){
            body.removeChild(div);
        }, 0);
    }
</script>

header.html

Основное меню сайта.

<nav class="navbar navbar-expand-md navbar-light bg-light">
  <div class="container">
    <a class="navbar-brand" href="{{ .Site.BaseURL }}">
      <img src="{{ .Site.BaseURL }}images/logo.png" alt="{{ .Site.Title }}">
    </a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          {{ range .Site.Menus.main }}
          <li class="nav-item">
          <a class="nav-link active" aria-current="page" href="{{ absURL .URL }}">
          {{ $text := print .Name | safeHTML }}
          {{ $text }}
          </a>
          </li>
          {{ end }}
      </ul>
    </div>
  </div>
</nav>

Отключим подчеркивание ссылок и ограничим высоту логотипа, добавим в style.css.

.navbar-brand img {
    height: 50px;
}

a {
  text-decoration: none;
}

Подвал сайта

В файл themes/mytheme/layouts/partials/footer.html добавим:

<footer class="footer mt-auto py-3 bg-light">
    <div class="container">
        <span class="text-muted footer text-center">Copyright (c) {{ now.Format "2006"}} {{ .Site.Author.name | markdownify }}</span>
    </div>
</footer>

script.html

Подключаем js скрипты.

<script src="{{ .Site.BaseURL }}js/bootstrap.bundle.min.js"></script>

Кнопка вверх

{{ if (ne .IsHome true) }}
  {{- partial "btntop.html" . -}}
{{ end -}}

В этих строчках определяется, что текущая страница не главная и только тогда подключается файл btntop.html, в котором простой скрипт для кнопки вверх.

<script type="text/javascript">
    window.onscroll = function() {scrollFunction()};
    
    function scrollFunction() {
        if (document.body.scrollTop > 250 || document.documentElement.scrollTop > 250) {
            document.getElementById("btnTop").style.display = "block";
        } else {
            document.getElementById("btnTop").style.display = "none";
        }
    }
    
    function topFunction() {
        document.body.scrollTop = 0;
        document.documentElement.scrollTop = 0;
    }
</script>

Сама кнопка определяется в начале страницы, а внешний вид настраивается в style.css.

<i class="bi bi-caret-up-fill bg-secondary text-white" onclick="topFunction()" id="btnTop" title="Вверх"></i>
#btnTop {
    display: none;
    position: fixed;
    bottom: 60px;
    right: 30px;
    z-index: 99;
    border: none;
    outline: none;
    cursor: pointer;
    padding: 10px;
    border-radius: 10px;
    font-size: 20px;
}

Блоки

Блоки в Hugo позволяют переопределить части основного шаблона. Например для отдельных страниц блок будет взят из single.html, а для списка статей на сайте из list.html.

Отдельные страницы

Определим шаблон по умолчанию для отдельных страниц, например для страницы - О проекте, layouts/_default/single.html.

{{ define "main" }}
<div class="row g-5">
    <div class="col-md-8">
        <h3 class="pb-4 mb-4 border-bottom">
            {{ .Title }}
        </h3>
        {{ partial "breadcrumb.html" . }}
        {{ if .Params.toc }}
        <div class="p-4 mb-3 bg-light rounded">
            {{- partial "toc.html" . -}}
        </div>
        {{ end }}
        <article class="post">
            {{ .Content }}
        </article>
    </div>
    <div class="col-md-4">
        <div class="p-4 mb-3 bg-light rounded">
            {{- partial "tags.html" . -}}
        </div>
    </div>
</div>
{{ end }}

Выводим заголовок статьи, далее "хлебные крошки" упрощающие навигацию по сайту.

<div class="mt-3">
  <nav aria-label="breadcrumb">
    <ol class="breadcrumb">
      {{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
    </ol>
    {{ define "breadcrumbnav" }}
    {{ if .p1.Parent }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 ) }}
    {{ else if not .p1.IsHome }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 ) }}
    {{ end }}
    <li{{ if eq .p1 .p2 }} class="breadcrumb-item active" aria-current="page" {{ end }}>
      <a href="{{ .p1.Permalink }}">{{ .p1.Title }}</a>
      </li>
      {{ end }}
  </nav>
</div>

Добавим в css.

.breadcrumb {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
}

.breadcrumb li {
  display: inline;
  white-space: nowrap;
}

.breadcrumb li + li:before {
  content: ">";
  padding: 0.3rem;
}

Выводим саму статью и если в заголовке статьи установлен параметр toc: true тогда Hugo сформирует содержание статьи на основе заголовков - toc.html.

<aside>
    <header>
        <h4>Содержание</h4>
    </header>
    {{.TableOfContents}}
</aside>

Также добавим вывод всех терминов таксономии.

{{/*
  Вывод терминов словаря таксономии
  в зависимости от частоты использования
*/}}
<aside>
  {{- range $name, $taxonomy := .Site.Taxonomies }}
    {{/* Проверка размера словаря таксономии */}}
    {{- if not (eq (len $taxonomy) 0) }}
    <div class="wrapper tagclouds">
      {{- range $key, $value := $taxonomy }}
        {{ $count := .Count }}
        {{/* Проверка уровня термина таксономии */}}
        {{ if ge $count 2 }}
          {{/* Ограничение размера уровня термина */}}
          {{ if ge $count 10 }}{{ $count = 10 }}{{ end }}
          <span class="tagclouds-term"><a href="{{ .Page.Permalink }}" class="tagclouds level{{ $count }}">
            {{ .Page.Title }}
          </a> </span>
        {{ end }}
      {{- end }}
    </div>
    {{- end }}
  {{- end }}
</aside>

И добавим в style.css параметры терминов как в модуле Drupal.

.wrapper.tagclouds {
  text-align: justify;
  margin-right: 1em;
}

.tagclouds.level1 {
  font-size: 1em;
}
.tagclouds.level2 {
  font-size: 1.2em;
}
.tagclouds.level3 {
  font-size: 1.4em;
}
.tagclouds.level4 {
  font-size: 1.6em;
}
.tagclouds.level5 {
  font-size: 1.8em;
}
.tagclouds.level6 {
  font-size: 2em;
}
.tagclouds.level7 {
  font-size: 2.2em;
}
.tagclouds.level8 {
  font-size: 2.4em;
}
.tagclouds.level9 {
  font-size: 2.6em;
}
.tagclouds.level10 {
  font-size: 2.8em;
}
.tagclouds.level11 {
  font-size: 3em;
}
.tagclouds.level12 {
  font-size: 3.2em;
}
.tagclouds.level13 {
  font-size: 3.4em;
}
.tagclouds.level14 {
  font-size: 3.6em;
}
.tagclouds.level15 {
  font-size: 3.8em;
}

Для страниц типа статья создадим отдельный шаблон articles/single.html.

{{ define "main" }}
<div class="row g-5">
    <div class="col-md-8">
        <h3 class="pb-4 mb-4 border-bottom">
            {{ .Title }}
        </h3>
        {{ partial "metadata.html" . }}
        <br>
        {{ partial "breadcrumb.html" . }}
        {{ if .Params.toc }}
        <div class="p-4 mb-3 bg-light rounded">
            {{- partial "toc.html" . -}}
        </div>
        {{ end }}
        <article class="blog-post">
            {{ .Content }}
            {{ partial "social.html" . }}
        </article>
    </div>
    <div class="col-md-4">
        <div class="p-4 mb-3 bg-light rounded">
            {{- partial "tags.html" . -}}
        </div>
        {{ $related := .Site.RegularPages.RelatedIndices . "tags" | first 3 }}
        {{ with $related }}
        <div class="p-4 mb-3 bg-light rounded">
            <h5>Рекомендуем</h5>
            <ul class="list-group list-group-flush">
                {{ range . }}
                <li class="list-group-item"><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
                {{ end }}
            </ul>
        </div>
        {{ end }}
    </div>
</div>
{{ end }}

Отличие от стандартного блока подключение metadata.html и social.html.

{{ $dateTime := .PublishDate.Format "2006-01-02" }}
{{ $dateFormat := .Site.Params.DateForm | default "Jan 2, 2006" }}
<i class="bi bi-calendar-check"></i> <time datetime="{{ $dateTime }}">{{ .PublishDate.Format $dateFormat }}</time>
{{ with .Params.tags }}
    <i class="bi bi-tags"></i>
    {{ range . }}
        {{ $href := print (absURL "tags/") (urlize .) }}
        <a class="badge bg-secondary text-uppercase" href="{{ $href }}">{{ . }}</a>
    {{ end }}
{{ end }}

Этот файл формирует строку с датой статьи и связанными тегами таксономии.

Перед подключением social.html переходим на сервис Yandex и выбираем нужные кнопки.

<script src="https://yastatic.net/share2/share.js"></script>
<div class="ya-share2" data-curtain data-size="l" data-shape="round" data-services="messenger,vkontakte,odnoklassniki,telegram,moimir"></div>

Таксономия

Для отображения терминов таксономии создадим шаблон блока в файле layouts/_default/terms.html.

{{ define "main" }}
<div class="row g-5">
    <div class="col-md-8">
        {{ with .Content }}
        <div class="pb-2 mb-3 border-bottom">
            {{- . -}}
        </div>
        {{ end }}
        <div class="wrapper tagclouds">
            {{ range .Data.Terms.Alphabetical }}
            <span class="tagclouds-term"><a href="{{ .Page.Permalink }}" class="tagclouds level{{ .Count }}">
                    {{ .Page.Title }}
                </a> </span>
            {{ end }}
        </div>
    </div>
    <div class="col-md-4">
        <div class="p-4 mb-3 bg-light rounded">
            {{- partial "tags.html" . -}}
        </div>
    </div>
</div>
{{ end }}

Списки

Шаблон из файла layouts/_default/list.html используется для отображения списков материалов из разделов сайта.

{{ define "main" }}
<div class="row g-5">
    <div class="col-md-8">
        {{ with .Content }}
        <div class="pb-2 mb-3 border-bottom">
            {{- . -}}
        </div>
        {{ end }}
        {{ range where .Paginator.Pages "Type" "!=" "page" }}
        <article class="post">
            <h3 class="post-title mb-1"><a class="title" href="{{ .RelPermalink }}">{{ .Title }}</a></h3>
            <p class="post-meta">{{ partial "metadata.html" . }}</p>
            <p>{{ .Summary }}</p>
            {{ end }}
        </article>
        {{ template "_internal/pagination.html" . }}
    </div>
    <div class="col-md-4">
        <div class="p-4 mb-3 bg-light rounded">
            {{- partial "tags.html" . -}}
        </div>
    </div>
</div>
{{ end }}

В раздел можно поместить файл _index.md, для описания самого раздела, также можно добавить описание терминов таксономии, создав соответствующие разделы и файлы. Для вывода этой информации добавим в блок проверку:

{{ with .Content }}
<div class="pb-2 mb-3 border-bottom">
{{- . -}}
</div>
{{ end }}

Далее формируем списком краткое содержание всех статей и выводим пагинацию из внутреннего шаблона Hugo.

{{ template "_internal/pagination.html" . }}

404

Для ошибки 404 свой шаблон блока.

{{ define "main" }}
<div class="row g-5">
  <div class="col-md-8">
    <h3 class="pb-4 mb-4 border-bottom">
      404
    </h3>
    <p>Сожалеем, но страница, которую Вы запрашиваете, не существует. Возможно она была удалена или перемещена, возможно
      Вы набрали неверный адрес.</p>
    <h5>Рекомендуем</h5>
    {{ $query := site.RegularPages }}
    {{ $count := len $query }}
    {{ if gt $count 0 }}
    <ul class="list-group list-group-flush">
      {{ range first 3 $query }}
      <li class="list-group-item"><a href="{{ .Permalink }}">{{ .Title }}</a></li>
      {{ end }}
    </ul>
    {{ end }}
  </div>
  <div class="col-md-4">
    <div class="p-4 mb-3 bg-light rounded">
      {{- partial "tags.html" . -}}
    </div>
  </div>
</div>
{{ end }}

Для Apache свою страницу ошибок можно прописать в файле .htaccess и разместить его в папке static

ErrorDocument 404 /404.html

Продолжение...