SLPのホームページを新たに作り直すプロジェクトが進行中です。
今回は新ホームページの紹介と合わせて、プロジェクトの概要を説明します。

この記事はSLP KBIT AdventCalendar 2022、24日目の記事です。

概要

今までのHPは、オンプレミスで動かしているサーバにWordPressを立てて運営していました。
2013年(2012年?)に公開されて以来、秘伝のタレの如く受け継がれてきましたが、そろそろアップデートが必要だろうということで今回新しく作り直します。

新しいHPは、Hugoという静的サイトジェネレータを使用し、Cloudflare Pagesでホスティングしています。

なぜ移行するのか

旧HPはテーマが古い上、学外から簡単に編集することができません。
またオンプレで動かしているため、サイトがダウンしたり応答がめちゃめちゃ遅かったりと、トラブルも多いんですよね。
記事の一覧もないので、訪問者にとっては快適とは言い難い状況です。
上記以外にも、将来的、金銭的な理由もありますが、ここでは割愛ということで…

うちのHPは新入生もよく見るでしょうから、いつでも最新の情報を安定して届ける仕組みが必要です。

そこで、新しいHPは以下のようにすることで、編集者と利用者の双方が利用しやすい環境を目指しています。

  • 外部のサービス(Cloudflare Page)にホスティングしてもらう
  • 記事はGitHubで管理する
  • 記事はMarkdownを用いて書く

使っている技術

Hugo

HugoはGo言語で実装された静的Webサイト構築用フレームワークです。

静的サイトジェネレータですので、データベースなどは使用せず、最終的にはすべてHTML、CSS、JSとして出力されます。
記事はMarkdown形式をサポートしており、ビルドした段階で自動的にサイト全体を構築してくれます。

HugoではWordPressのようにテーマを設定することができ、手軽にきれいな外観をもったサイトを構築できるのが特徴です。
本HPも、Tranquilpeakというテーマをカスタマイズして使用しています。

Cloudflare Pages

Cloudflare Pagesは、その名の通りCloudflareが提供している静的サイトのホスティングサービスです。

2021年に正式公開された比較的新しいサービスであり、その分GitHub PagesやNetlifyなどと比較すると機能的にはかなり充実しています。

特に、転送量が無制限であるのと、サイトの容量が2万ファイルである点、Hugoのビルド設定がとても簡単なところはかなり嬉しいですね。
例えばGithub Pagesなら、転送量100GB/月でサイトの容量は1GBが上限であるのに加え、HugoのビルドもActionsから指定する必要があってかなり窮屈です。

また、Cloudflare Pagesはブランチ毎にサブドメインで分けてビルドされ、mainブランチ以外のページは認証機能をつけることもできます。
これなら、サブブランチで安心して記事を書けますね。


サイトの構築

構築といっても1から作るわけでは無いので簡単です。
hugo new <適当な名前>というコマンドでプロジェクトを生成した後に、layoutsディレクトリにテーマのGitHubリポジトリをクローンしてくるだけです。

あとは画像や説明を差し替えて、./config.tomlを編集すれば大部分は完成です。
ここらへんは公式のドキュメントや、検索でも沢山引っ掛かりますので細かい部分は割愛します。

HPの様子は実際にこのサイトを見て回ってもらうこととして、以下では細々とした機能の説明を行います。

コードブロックにタイトルを追加したい

MarkDownでコードを記述できるのは良いのですが、コードブロックにQiitaみたいにタイトルをつけたかったので新たに機能を追加します。
カスタムJS機能を使って追加のjsを読み込みます。

config.toml
1
2
3
4
[[params.customCSS]]
  href = "/css/mystyle.css"
[[params.customJS]]
  src = "/js/code-title.js"

コードはcodeタグで囲まれるため、それをDOMで指定し、コードブロックとインラインコード用にそれぞれidを付与します。
また、コードブロックに関しては、その要素の前にタイトルを表示するためのdivタグを挿入します。

code-title.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"use strict"

const list = document.body.getElementsByTagName("code");

for(let i = 0; i < list.length; i++){
  const code = list[i]
  const codeName = code ? code.className.split(":")[1] : null;
  const pre = code.parentElement

  if (pre.tagName === "P") {
    code.setAttribute("id", 'inline-code');
  } else if (codeName && !pre.classList.length) {
    const div = document.createElement('div');
    div.setAttribute("id", 'code-title');
    div.textContent = codeName;
    code.setAttribute("id", 'named-code');
    code.before(div);
  } else {
    code.setAttribute("id", 'code');
  }
}

あとはcssで装飾してあげます。
先程jsでidを付与しているため、それをセレクトし、それぞれに色やパディングなどを設定しています。
コードタイトルとコードブロックは重なる必要があるため、コードタイトル側のpositionabsoluteとして位置を調整しています。

mystyle.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#inline-code {
  color: rgb(67, 67, 67);
}

pre {
  position: relative;
}

#named-code {
  padding-top: 2.7em;
}

#code-title {
  position: absolute;
  top: 0px;
  left: 0px;
  padding: 0 0.6em;
  font-size: 1.4rem;
  line-height: 2em;
  background-color: rgb(234, 234, 234);
  color: rgb(64, 64, 64);
}

以上のような実装をすることで、コードブロックにタイトルを付与することができます。

hogehoge

ちなみにこれはあくまでもMarkDownを用いたコードブロックの話です。
このテーマ(Tranquilpeak)では、Hugoのショートコード機能を用いたより高機能なコードブロックが用意されています。

ショートコードについて

ショートコードとは、Markdown中で呼び出せる複雑なレイアウトを定義できる機能です。
Hugoにはビルトインのショートコードも用意されており、Youtubeを埋め込んだり、ツイートを埋め込んだりといった事ができます。
また、自分でショートコードを定義したり、テーマによっては独自のショートコードが実装されている場合もあります。
書き方はhtmlにgolangを埋め込むイメージです。

リンクカードを埋め込みたい

Qiitaの記事みたいにリンクをカード形式で表示させたいのでlink-cardという名前でショートコードを作成します。 といっても、はてなのリンクカードのコードを使えば簡単に作ることができます。 引数で渡されるURLをiframeタグのsrcに渡してあげます。

link-card.html
1
2
{{- $url := (.Get 0) -}}
  <iframe class="hatenablogcard" style="display:block;width:100%;height:auto;margin:2rem 0;" title="%title%" src="https://hatenablog-parts.com/embed?url={{- $url -}}" frameborder="0" scrolling="no"></iframe>

あとはこれを呼び出せば、

{{< link-card "https://hatenablog.com/" >}}

以下のように表示できます。

執筆者を表示できるようにしたい

このテーマは1人で運用することを想定しているようで、記事に執筆者を表示することができません。
カテゴリと同じように執筆者もメタデータとして表示できるようにしましょう。

まずは、メタデータを表示しているファイルの内容をオーバーライドして、執筆者を表示するためのファイル(authors.html)を読み込むようにします。

layouts/partials/post/meta.html
1
2
3
4
5
6
7
8
9
10
11
{{ if not (eq .Params.showMeta false) }}
  <div class="postShorten-meta post-meta">
    {{ if not (eq .Params.showDate false)  }}
      <time datetime="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
        {{ partial "internal/date.html" . }}
      </time>
    {{ end }}
    {{ partial "post/authors.html" . }}
    {{ partial "post/category.html" . }} // 執筆者を表示するためのファイルを読み込む
  </div>
{{ end }}

記事のメタデータとして新たにauthorsを定義し、それを以下のファイルで読み込むことで、執筆者を示す文字列を記事中に挿入します。
.Params.authorsでページ変数中のリストauthorsを指定し、その中身を1つずつ取り出して、文字列としてつなげていきます。

layouts/partials/post/authors.html
1
2
3
4
5
6
7
8
9
{{ with .Params.authors }}
  {{ $authorsLen := len . }}
  {{ if gt $authorsLen 0 }}
    &emsp;<span>書いた人:</span>
    {{ range $k, $v := . }}
      <a class="category-link" href="{{ . | printf "%s%s" "/authors/" | urlize | lower | relLangURL }}">{{ . }}</a>{{ if lt $k (sub $authorsLen 1) }}, {{ else }}{{ end }}
    {{ end }}
  {{ end }}
{{ end }

名前をクリックすると、その執筆者が含まれているページ一覧が表示されます。
これは、もともとカテゴリーで使われていたものを流用しています。

layouts/taxonomy/author.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
{{ partial "head.html" . }}
  <body>
    <div id="blog">
      {{ partial "header.html" . }}
      {{ partial "sidebar.html" . }}
      {{ if or (not (isset .Site.Params "authorPagination")) (.Site.Params.authorPagination) }}
        {{ partial "post/header-cover.html" . }}
      {{ end }}
      <div id="main" data-behavior="{{ .Scratch.Get "sidebarBehavior" }}"
        class="{{ with .Params.coverimage }}hasCover{{ end }}
               {{ if eq .Params.covermeta "out" }}hasCoverMetaOut{{ else }}hasCoverMetaIn{{ end }}
               {{ with .Params.coverCaption }}hasCoverCaption{{ end }}">
        {{ if or (not (isset .Site.Params "authorPagination")) (.Site.Params.authorPagination) }}
          <section class="postShorten-group main-content-wrap">
            {{ $paginator := .Paginate (where .Data.Pages "Type" "in" site.Params.mainSections) }}
            {{ range $paginator.Pages }}
              {{ .Render "summary" }}
            {{ end }}
            {{ partial "pagination.html" . }}
          </section>
        {{ else }}
          <div id="archives" class="main-content-wrap">
            <form id="filter-form" action="#">
              <input name="date" type="text" class="form-control input--xlarge" placeholder="{{ i18n "global.search_date" }}" autofocus="autofocus">
            </form>
            {{ partial "archive-post.html" (where .Data.Pages "Type" "in" site.Params.mainSections) }}
          </div>
        {{ end }}
        {{ partial "footer.html" . }}
      </div>
    </div>
{{ partial "foot.html" . }}

執筆者検索ができるようにしたい

このHPは静的なファイルのみで構成されており、データベースなどは用いていないので、一般的な方法では検索を行うことができません。
そこで、このテーマでは予めインデックスを作成した上で、JavaScriptを用いてカテゴリやタグなどを擬似的に絞り込むことができる機能が盛り込まれています。

これを流用して、執筆者でも検索できるようにします。
テーマの中のlayouts/taxonomy/tag.terms.htmlをコピーしてプロジェクトのlayouts/txonomy/author.terms.htmlを作成します。

layouts/taxonomy/author.terms.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{{ partial "head.html" . }}
  <body>
    <div id="blog">
      {{ partial "header.html" . }}
      {{ partial "sidebar.html" . }}
      {{ partial "post/header-cover.html" . }}
      <div id="main" data-behavior="{{ .Scratch.Get "sidebarBehavior" }}"
        class="{{ with .Params.coverImage }}hasCover{{ end }}
               {{ if eq .Params.coverMeta "out" }}hasCoverMetaOut{{ else }}hasCoverMetaIn{{ end }}
               {{ with .Params.coverCaption }}hasCoverCaption{{ end }}">

        <div id="authors-archives" class="main-content-wrap">
          <form id="filter-form" action="#">
            <input name="author" type="text" class="form-control input--xlarge" placeholder="執筆者検索" autofocus="autofocus">
          </form>
          <h4 class="archive-result text-color-base text-xlarge"
              data-message-zero="見つかりませんでした。"
              data-message-one="1 人の執筆者が見つかりました。"
              data-message-other="{n} 人の執筆者が見つかりました。"></h4>

            <section>
<!-- 省略 -->

また、フィルタを実現するため、テーマディレクトリ中のsrc/js/tags-filter.jsを流用し、以下のようなファイルを記述します。
これを、追加のjsとして読み込ませます。

処理内容としては、先程のフォームの内容を受け取り、情報を絞り込んだ上でページ情報を更新しています。

layouts/taxonomy/author.terms.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
(function($) {
  'use strict';

  /**
   * AuthorsFilter
   * @param {String} authorsArchivesElem
   * @constructor
   */
  var AuthorsFilter = function(authorsArchivesElem) {
    this.$form = $(authorsArchivesElem).find('#filter-form');
    this.$inputSearch = $(authorsArchivesElem + ' #filter-form input[name=author]');
    this.$archiveResult = $(authorsArchivesElem).find('.archive-result');
    this.$authors = $(authorsArchivesElem).find('.tag');
    this.$posts = $(authorsArchivesElem).find('.archive');
    this.authors = authorsArchivesElem + ' .tag';
    this.posts = authorsArchivesElem + ' .archive';
    // Html data attribute without `data-` of `.archive` element which contains the name of author
    this.dataAuthor = 'tag';
    this.messages = {
      zero: this.$archiveResult.data('message-zero'),
      one: this.$archiveResult.data('message-one'),
      other: this.$archiveResult.data('message-other')
    };
  };

  AuthorsFilter.prototype = {
    /**
     * Run AuthorsFilter feature
     * @return {void}
     */
    run: function() {
      var self = this;

// tags-filter.jsとほぼ同じ為省略

  $(document).ready(function() {
    if ($('#authors-archives').length) {
      var authorsFilter = new AuthorsFilter('#authors-archives');
      authorsFilter.run();
    }
  });
})(jQuery);

これで執筆者で検索することができるようになりました。

この他基本的な使い方はここに追加していきます。


今後の計画

基本的な機能はできているので、あとやることはGitHubのドキュメンテーションを書くことと、過去記事の移植ぐらいですかね。
まあ、大きいサイズの画像をリサイズしたり、webp形式に変換する機能をつけたいですが、それは追々ということで…
あとトップページの画像が屋島の景色になっちゃってるので、いい感じのものを持っていたらください。

ちなみに今アクセスしているURLは本番のものではなく、実際にはカスタムドメインを設定して、従来のドメインでアクセスできるようになっているはずです。
移行しても旧HPとドメインは変わらないのでご安心ください。

おわりに

ということで、SLPのHP移行するよってお知らせと、簡単な概要の説明でした。
ほんとは移行完了していたかったのですが、他のインフラプロジェクトとの兼ね合いで滞っている状況です…
今年度中には移行を完了させ、沢山新入部員を迎えたいですね!