メインコンテンツへスキップ

Hugoテーマの選定とレイアウトカスタム

·
ポートフォリオサイトをHugoで作成した - This article is part of a series.
Part 2: This Article

なにこれ
#

  • 公開されているHugoテーマをカスタムしていい感じにしたよ

モチベ
#

  • スタートページにリアルとバーチャルのどちらのプロフィールを表示したい

プロフィールの管理について、私はリアルとバーチャルの存在を分離して捉えています。

Hugo Themeの選定
#

Hugoのテーマは ここにたくさん掲載されています。

詳細なプロフィールは別々に管理した方が良さげだったので、リアルとバーチャル用に次のテーマを選定しました。

  • スタートページ・バーチャルのポートフォリオ: Blowfish
  • リアルのポートフォリオ: Academic

Blowfishのページレイアウトのカスタム
#

Blowfishユーザーのうち v-y-sさんの見た目がかなり良かったためこれを参考にして、 heroをベースに作成していきます。

スタートページにリアルとバーチャルの簡単なプロフィールを表示したかったのですが、デフォルトでは対応していません。 そこで、プロフィールをスライドショーのように遷移させるカスタムレイアウトを作成しました。

Blowfishは複数著者をサポートしている関係でdata/authorsに各々のプロフィールを配置できます。
{{ .Site.Data.authors }}から読み込むとアルファベット順に表示されしまうのでparamhomepageOrderを追加して表示順を制御できるようにしました。

layouts/partials/home/custom.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
{{ $disableImageOptimization := .Site.Params.disableImageOptimization | default false }}
<article class="max-w-full prose dark:prose-invert">
  <div class="fixed inset-x-0 top-0" style="z-index: -10">
    <img id="switchableBgImg" class="w-full h-[1000px] object-cover m-0 nozoom" src="" />
  </div>
  <div
    class="fixed opacity-1 inset-x-0 top-0 h-full single_hero_background nozoom"
    style="backdrop-filter: blur(8px)"
  ></div>
  <div class="relative">
    <div class="absolute inset-x-0 bottom-0 h-1/2 bg-gray-100"></div>
    <div class="mx-auto max-w-7xl p-0">
      <div class="relative shadow-xl sm:overflow-hidden rounded-2xl">
        <div class="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8" >
          <!-- プロフィールの背景にBlurを強くして見やすくする -->
          <div
            class="absolute opacity-1 inset-x-0 top-0 h-full single_hero_background nozoom"
            style="backdrop-filter: blur(12px)"
          ></div>
          <div
            class="relative px-4 py-16 sm:px-6 sm:py-24 lg:py-32 lg:px-8 container mx-auto flex flex-col items-center justify-center text-center"
          >
          <!-- data/authors以下のプロフィールを読み込む -->
            {{ $authorsData := .Site.Data.authors }}
            {{ range $author := .Site.Params.homepageOrder }}
            {{ $authorData := index $authorsData $author }}
              {{- if $authorData -}}
                <div class="absolute flex flex-col items-center justify-center text-center opacity-0 slide">
                  {{ with $authorData.image }}
                    {{ $authorImage := "" }}
                    {{ if or (strings.HasPrefix . "http:") (strings.HasPrefix . "https:") }}
                      {{ $authorImage = resources.GetRemote . }}
                    {{ else }}
                      {{ $authorImage = resources.Get . }}
                    {{ end }}
                    {{ if $authorImage }}
                      {{ if not $disableImageOptimization }}
                        {{ $authorImage = $authorImage.Fill "288x288" }}
                      {{ end }}
                      <img
                        class="mb-2 rounded-full h-36 w-36"
                        width="144"
                        height="144"
                        alt="{{ $authorData.name | default " Author" }}"
                        src="{{ $authorImage.RelPermalink }}"
                      />
                    {{ end }}
                  {{ end }}
                  <h1 class="mb-2 text-4xl font-extrabold">
                    {{ $authorData.name }}
                  </h1>
                  <!-- headlineからbioに変更+h2のクラスにbioを追加 -->
                  {{ with $authorData.bio }}
                    <h2 class="bio mt-0 mb-0 text-xl text-neutral-100 dark:text-neutral-200">
                      {{ . | markdownify | emojify }}
                    </h2>
                  {{ end }}
                  <div class="mt-3 mb-10 text-2xl">
                    {{ with $authorData.social }}
                      <div class="flex flex-wrap">
                        {{ range $links := . }}
                          {{ range $name, $url := $links }}
                            <a
                              class="px-1 hover:text-primary-300 text-primary-200"
                              href="{{ $url }}"
                              target="_blank"
                              aria-label="{{ $name | title }}"
                              rel="me noopener noreferrer"
                              >{{ partial
                                "icon.html" $name
                              }}</a
                            >
                          {{ end }}
                        {{ end }}
                      </div>
                    {{ end }}
                  </div>
                </div>
              {{end}}
            {{end}}
            <section class="prose prose-invert">{{ .Content | emojify }}</section>
          </div>
        </div>
      </div>
    </div>
  </div>
    {{ $jsHome := resources.Get "js/home.js" | resources.Minify | resources.Fingerprint "sha512" }}
    <script
      defer
      type="text/javascript"
      src="{{ $jsHome.RelPermalink }}"
      integrity="{{ $jsHome.Data.Integrity }}"
    ></script>
    <script src="https://unpkg.com/typeit@8.7.1/dist/index.umd.js"></script>
  </div>
</article>
<section>
  {{ partial "recent-articles/main.html" . }}
</section>
config/_default/language.ja.toml
1
2
[param]
homepageOrder = ["startpage", "sumi", "sumi-sumi"]

javascript12行で書くfadeIn, fadeOutスライドショーにて紹介されていたプログラムのターゲットを画像では無くslideクラスに向けただけです。
タイプライターのようなテキスト表示ができる Typeitの起動を追加しています。

assets/js/home.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
let slideIndex = 0
let shotTime = 0
const startpageShowTime = 8000
const profileShowTime = 11000
const fadeInDelay = 1000
const fadeOutDelay = 1200

const typeitStartDelay = 500
const typeitSpeed = 100

showSlides()

function toMSec2Sec(msec) {
  return msec / 1000
}

function fadeOut(element) {
  element.style.transition = "opacity " + String(toMSec2Sec(fadeOutDelay)) + "s"
  element.style.opacity = "0"
}
function fadeIn(element) {
  element.style.transition = "opacity " + String(toMSec2Sec(fadeInDelay)) + "s"
  element.style.opacity = "1"
}

function showSlides() {
  // スライドの要素を取得
  const slides = document.querySelectorAll(".slide")
  const bios = document.querySelectorAll(".bio")

  slideIndex++

  // スライドの数よりもslideIndexが大きくなった場合、最初のスライドに戻る
  if (slideIndex > slides.length) {
    slideIndex = 1
  }

  if (slideIndex == 1) {
    showTime = startpageShowTime
  } else {
    showTime = profileShowTime
  }

  // 現在のスライドを表示(opacityを1に設定)
  fadeIn(slides[slideIndex - 1])

  // 1つ前のスライドを非表示(opacityを0に設定)
  fadeOut(slides[slideIndex - 2 < 0 ? slides.length - 1 : slideIndex - 2])

  // headline をタイプライターのように表示
  new TypeIt(bios[slideIndex - 1], {
    speed: typeitSpeed,
    startDelay: typeitStartDelay,
    waitUntilVisible: true,
  }).go()

  // 一定時間後に再度showSlides関数を呼び出す(次のスライドに進むための処理)
  setTimeout(showSlides, showTime)
}

カラーテーマ変更時の背景画像を差し替え
#

ホームページの背景を画像にするとカラーテーマによって文字の視認性が大きく落ちることに気づきました。
そのため、カラーテーマを変更したら背景画像も差し替えるようにしました。

Blowfishのassets/js/appearance.jsをプロジェクト配下のassets/jsにコピーして変更しています。 プログラムは元々存在したupdateLog()を踏襲して必要な情報に書き換えてswitchableBgImgクラスを参照するようにしています。

またparamsecondaryBackgroundImage を追加しています。
Blowfishはparams.tomldefaultAppearanceで指定されたものがprimaryテーマになります。 背景も同様にdefaultBackgroundImageがデフォルトでsecondaryBackgroundImageがカラーテーマ変更時の背景画像になります。

assets/js/appearance.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
...
window.addEventListener("DOMContentLoaded", (event) => {
  const switcher = document.getElementById("appearance-switcher");
  const switcherMobile = document.getElementById("appearance-switcher-mobile");

  updateMeta();
  this.updateLogo?.(getTargetAppearance());
  this.updateBg?.(getTargetAppearance());  // 追記

  if (switcher) {
    switcher.addEventListener("click", () => {
      document.documentElement.classList.toggle("dark");
      var targetAppearance = getTargetAppearance();
      localStorage.setItem(
        "appearance",
        targetAppearance
      );
      updateMeta();
      this.updateLogo?.(targetAppearance);
      this.updateBg?.(targetAppearance);  // 追記
    });
    switcher.addEventListener("contextmenu", (event) => {
      event.preventDefault();
      localStorage.removeItem("appearance");
    });
  }
  if (switcherMobile) {
    switcherMobile.addEventListener("click", () => {
      document.documentElement.classList.toggle("dark");
      var targetAppearance = getTargetAppearance();
      localStorage.setItem(
        "appearance",
        targetAppearance
      );
      updateMeta();
      this.updateLogo?.(targetAppearance);
      this.updateBg?.(targetAppearance);  // 追記
    });
    switcherMobile.addEventListener("contextmenu", (event) => {
      event.preventDefault();
      localStorage.removeItem("appearance");
    });
  }
});

...

// 追記
{{ $primaryBg := "" }}
{{ $secondaryBg := "" }}
{{ with .Site.Params.defaultBackgroundImage }}
  {{ $primaryBg = resources.Get . }}
{{ end }}
{{ with .Site.Params.homepage.homepageImage }}
  {{ $primaryBg = resources.Get . }}
{{ end }}
{{ with .Site.Params.secondaryBackgroundImage }}
  {{ $secondaryBg = resources.Get . }}
{{ end }}
{{ with .Site.Params.homepage.secondaryHomepageImage }}
  {{ $secondaryBg = resources.Get . }}
{{ end }}
{{ if and (not (eq $primaryBg "")) (not (eq $secondaryBg "")) }}
var updateBg = (targetAppearance) => {
  var elems;
  elems = document.querySelectorAll("#switchableBgImg")
  targetBGPath =
    targetAppearance == "{{ .Site.Params.DefaultAppearance }}" ?
    "{{ $primaryBg.RelPermalink }}" : "{{ $secondaryBg.RelPermalink }}"
  for (const elem of elems) {
    elem.setAttribute("src", targetBGPath)
  }
}
{{ end }}

参考
#

ポートフォリオサイトをHugoで作成した - This article is part of a series.
Part 2: This Article