Featured image of post Stackをカスタマイズする

Stackをカスタマイズする

hugoのテーマ"Stack"をカスタマイズする

Stackとは

現在このサイトで使っているhugoのテーマです。
おしゃれです。シンプルでいいですよね。

とりあえずまとめる程度のものなので、不備があるかも。

インストール

git cloneする

git clone https://github.com/CaiJimmy/hugo-theme-stack/ themes/hugo-theme-stack

シンプルでいいですね。

サブモジュール

git submodule add https://github.com/CaiJimmy/hugo-theme-stack/ themes/hugo-theme-stack

私はこっちを使っています。
なおCloudflarePagesでデプロイしてるのでhttpsではなくsshで。

テーマを使う

設定ファイル(例:hugo.yml)に以下を追記、もしくは編集する。

theme: hugo-theme-stack

これでOK。

設定ファイルの例

あくまで例ですので、項目などについては公式ドキュメントをご覧ください。

# 基本設定
baseURL: https://example.org/
languageCode: ja
title: サイトの名前
theme: hugo-theme-stack

# コンテンツの言語
DefaultContentLanguage: ja

# パーマリンクの設定
permalinks:
  post: /p/:year/:slug
  page: /:slug

# テーマの設定
params:
  featuredImageField: image # フロントマターのフィーチャー画像のフィールド
  rssFullContent: true # rssでページの全ての内容を出力するかどうか
  # 日時のフォーマットを設定
  dateFormat:
    published: 2006/01/02
    lastUpdated: 2006/01/02 15:04 JST
  # サイドバーの設定
  sidebar:
    emoji: 😎 # 絵文字
    subtitle: 猫大好き〜 # サブタイトル
    # アイコン
    avatar:
      enabled: true
      local: true
      src: img/icon.webp
  # 右側に置かれる要素
  widgets:
    homepage:
      - type: search
      - type: archives
        params: 
          limit: 10
      - type: categories
        params:
          limit: 5
      - type: tag-cloud
    page:
      - type: search
      - type: toc
      - type: archives
        params: 
          limit: 10
      - type: categories
        params:
          limit: 5
      - type: tag-cloud
  # 画像処理の設定
  imageProcessing:
    cover:
      enabled: false
    content:
      enabled: false

# メニューの初期化
menu:
  main: []

# マークアップの設定
markup:
  goldmark:
    renderer:
      unsafe: true
  highlight:
    noClasses: true
    codeFences: true
    guessSyntax: true
    lineNos: false
    lineNumbersInTable: false
    tabWidth: 4

フォントを変える

layouts/partials/head/custom.html を作成します。
今回は私の愛しているフォントInterとNoto Sans JPを使います。

以下を追記

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">

<style>
    :root {
        --base-font-family: 'Inter', 'Noto Sans JP', sans-serif;
    }
</style>

違和感なく美しいフォントになります。

リンクカードを実装する

Info
このセクションはかなり雑な実装があるので、すごい人は良いものに書き換えてください。

頑張って作りました;;

注意
最新のhugoじゃ動きませんでした。
当サイトはv0.129.0を使用しています。

ショートコードのhtmlファイル

layouts/shortcodes/link.htmlを作成し以下を追記する。

Info
コードには一部AIによる加筆があります。
{{- $url := (.Get "url") -}}
{{- $target_url := urls.Parse $url -}}
{{- $type := (.Get "type") -}}

{{- $title := "" -}}
{{- $favicon_url := "" -}}
{{- $ogp_image := "" -}}

{{- with $result := resources.GetRemote $url -}}
    {{- with $result.Err -}}
        {{- $title = (print $url "にアクセスできませんでした") -}}
    {{- else -}}
        <!-- headを取得 -->
        {{- $head_matches := findRE "<head[^>]*?>(.|\n)*?</head>" $result.Content -}}
        {{- if gt (len $head_matches) 0 -}}
            {{- $head := index $head_matches 0 -}}

            <!-- headからタイトルを取得 -->
            {{- $title_matches := findRE "<title.*?>(.|\n)*?</title>" $head -}}
            {{- if gt (len $title_matches) 0 -}}
                {{- $title = index $title_matches 0 | replaceRE "</?title>" "" -}}
            {{- end -}}

            <!-- Googleのfaviconサービスを使用してfaviconを取得 -->
            {{- $favicon_url = print "https://www.google.com/s2/favicons?domain=" $target_url.Hostname "&sz=64" -}}

            <!-- OGP画像を取得 (正規表現を改善) -->
            {{- $ogp_meta_tags := findRE "<meta[^<>]*property=[\"']og:image[\"'][^<>]*?>" $head -}}
            {{- if gt (len $ogp_meta_tags) 0 -}}
                {{- $ogp_meta := index $ogp_meta_tags 0 -}}
                <!-- contentの値だけを抽出するためのパターン -->
                {{- $content_match := findRE "content=[\"']([^\"']+)[\"']" $ogp_meta -}}
                {{- if gt (len $content_match) 0 -}}
                    <!-- キャプチャグループを使用して引用符の中身だけを取得 -->
                    {{- $content_parts := split (index $content_match 0) "'" -}}
                    {{- if gt (len $content_parts) 1 -}}
                        {{- $ogp_image = index $content_parts 1 -}}
                    {{- else -}}
                        {{- $content_parts := split (index $content_match 0) "\"" -}}
                        {{- if gt (len $content_parts) 1 -}}
                            {{- $ogp_image = index $content_parts 1 -}}
                        {{- end -}}
                    {{- end -}}
                    
                    <!-- 正しいURLであることを確認 -->
                    {{- if and $ogp_image (ne $ogp_image "") -}}
                        {{- if eq (strings.Substr $ogp_image 0 1) "/" -}}
                            {{- $ogp_image = print $target_url.Scheme "://" $target_url.Hostname $ogp_image -}}
                        {{- else if not (gt (len (findRE "^https?://" $ogp_image)) 0) -}}
                            {{- $ogp_image = print $target_url.Scheme "://" $target_url.Hostname "/" $ogp_image -}}
                        {{- end -}}
                    {{- end -}}
                {{- end -}}
            {{- end -}}
        {{- end -}}
    {{- end -}}
{{- end -}}

<!-- アイコンとラベルのマッピング -->
{{- $icon := "" -}}
{{- $label := "" -}}
{{- if $type -}}
    {{- $icon_map := dict "sankou" "ti ti-book" "download" "ti ti-download" "official" "ti ti-star" "awasete" "ti ti-books" "osusume" "ti ti-heart" "sns" "ti ti-message-chatbot" -}}
    {{- $label_map := dict "sankou" "参考" "download" "ダウンロード" "official" "公式サイト" "awasete" "合わせて読みたい" "osusume" "おすすめ" "sns" "SNS" -}}
    {{- with index $icon_map $type -}}
        {{- $icon = . -}}
    {{- end -}}
    {{- with index $label_map $type -}}
        {{- $label = . -}}
    {{- end -}}
{{- end -}}
<div class="link-card-wrapper">
  <a href="{{- htmlUnescape $target_url -}}" target="_blank" class="link-card-anchor">
    <div class="link-card">
      {{- if $ogp_image -}}
      <div class="link-card-image">
        <img class="link-card-ogp" src="{{- $ogp_image -}}" alt="">
      </div>
      {{- end -}}
      <div class="link-card-overlay">
        <div class="link-card-title">
          {{- $title | htmlUnescape | truncate 100 -}}
        </div>
        <div class="link-card-hostname">
          {{- if $favicon_url -}}
          <div class="link-card-hostname-img">
            <img src="{{- $favicon_url -}}" alt="">
          </div>
          {{- end -}}
          <span>
            {{- $target_url.Hostname -}}
          </span>
        </div>
        {{- if and $type $label $icon -}}
        <span class="card-type">{{- $label -}} <i class="{{- $icon -}}"></i></span>
        {{- end -}}
      </div>
    </div>
  </a>
</div>

スタイルを設定

assets/scss/custom.scssに以下を追記。

/* リンクカードのラッパー */
.link-card-wrapper {
    margin-top: 1em;
    margin-bottom: 1em;
}

/* アンカータグのスタイルリセット */
.link-card-anchor {
    text-decoration: none;
    color: inherit;
    display: block;
    width: 100%;
}

/* リンクカードの基本スタイル */
.link-card {
    display: flex;
    flex-direction: row;
    border: solid 1px rgba(0, 0, 0, 0.15);
    border-radius: 8px;
    transition: .3s;
    overflow: hidden;
    position: relative;
    color: var(--card-text-color-main);
}

/* マウスホバー時の挙動 */
.link-card:hover {
    opacity: 0.9;
    box-shadow: 0 0px 8px rgba(0, 0, 0, 0.1);
    transition: 0.3s;
    border-radius: 4px;
}

/* OGP画像用のスタイル */
.link-card-image {
    width: 25%;
    aspect-ratio: 16/9;
    border-right: 1px solid rgba(0, 0, 0, 0.15);
    overflow: hidden;
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

.link-card-image::after {
    position: absolute;
    font-family: "tabler-icons";
    content: '\ea99';
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, .5);
    opacity: 0;
    transition: all .3s;
    display: flex;
    justify-content: center;
    align-items: center;
    color: white;
    font-size: 1.5em;
    backdrop-filter: blur(4px);
}

.link-card-ogp {
    flex: 1;
    height: 100%;
    width: 100%;
    object-fit: cover;
    object-position: center;
    transition: all .3s;
    position: relative;
}

.link-card:hover .link-card-ogp {
    transform: scale(1.1);
}

.link-card:hover .link-card-image::after {
    opacity: 1;
}

.link-card-overlay {
    padding: 1em;
    display: flex;
    flex-direction: column;
    justify-content: center;
    /* gap: 0.5em; */
    width: 75%;
}

/* タイトルのスタイル */
.link-card-title {
    font-weight: bold;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ホスト名部分のスタイル */
.link-card-hostname {
    display: flex;
    align-items: center;
    height: 24px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-top: 1rem;
}

.link-card-hostname-img {
    height: 100%;
    display: flex;
    align-items: center;
}

.link-card-hostname-img img {
    width: 24px;
    height: 24px;
    margin-right: 8px;
}

.link-card-hostname span {
    font-size: 0.9em;
}

/* カードタイプラベルのスタイル */
.card-type {
    position: absolute;
    bottom: 0;
    right: 0;
    background-color: #333;
    color: #fff;
    padding: .4em 1.3em;
    border-radius: 8px 0 0 0;
    font-size: 12px;
}

.card-type i {
    margin-left: 2px;
    font-weight: normal;
}

/* スマホ表示のためのメディアクエリ */
@media screen and (max-width: 767px) {
    .link-card {
        flex-direction: column;
    }

    .link-card-image {
        width: 100%;
        aspect-ratio: 16/9;
        border-right: none;
        border-bottom: 1px solid rgba(0, 0, 0, 0.15);
    }

    .link-card-overlay {
        width: 100%;
    }

    /* モバイル表示ではタイトルを2行までに制限 */
    .link-card-title {
        white-space: normal;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }
}

/* タブレット表示のための微調整 */
@media screen and (min-width: 768px) and (max-width: 1024px) {
    .link-card-image {
        width: 30%;
    }

    .link-card-overlay {
        width: 70%;
    }
}

アイコンを読み込む

当サイトではTabler Iconsを使用しています。

layouts/partials/head/custom.htmlに以下を追記。

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.31.0/dist/tabler-icons.min.css" />

使い方

Info
[]{}に置き換えるように。
[[< link url="https://google.com" type="official" >]]

urlには完全なURLを入れ、typeは

<!-- アイコンとラベルのマッピング -->
{{- $icon := "" -}}
{{- $label := "" -}}
{{- if $type -}}
    {{- $icon_map := dict "sankou" "ti ti-book" "download" "ti ti-download" "official" "ti ti-star" "awasete" "ti ti-books" "osusume" "ti ti-heart" "sns" "ti ti-message-chatbot" -}}
    {{- $label_map := dict "sankou" "参考" "download" "ダウンロード" "official" "公式サイト" "awasete" "合わせて読みたい" "osusume" "おすすめ" "sns" "SNS" -}}
    {{- with index $icon_map $type -}}
        {{- $icon = . -}}
    {{- end -}}
    {{- with index $label_map $type -}}
        {{- $label = . -}}
    {{- end -}}
{{- end -}}

ここに対応します。

mermaidを使えるようにする

これもショートコードで。

layouts/shortcodes/mermaid.htmlを作成し以下を追記。

<!-- layouts/shortcodes/mermaid.html -->
{{ if not (.Page.Scratch.Get "mermaidLoaded") }}
  <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      mermaid.initialize({
        startOnLoad: true,
        theme: 'default',
        securityLevel: 'loose'
      });
    });
  </script>
  {{ .Page.Scratch.Set "mermaidLoaded" true }}
{{ end }}

<div class="mermaid">
  {{.Inner}}
</div>

使い方

Info
[]{}に置き換えるように。
[[< mermaid >]]
graph TD
    A[Enter Chart Definition] --> B(Preview)
    B --> C{decide}
    C --> D[Keep]
    C --> E[Edit Definition]
    E --> B
    D --> F[Save Image and Code]
    F --> B
[[< /mermaid >]]

これは下のようになる。

graph TD A[Enter Chart Definition] --> B(Preview) B --> C{decide} C --> D[Keep] C --> E[Edit Definition] E --> B D --> F[Save Image and Code] F --> B

グラフなどについては公式を参照。

終わり

終わりです。実はもっとやってるんだけどねw

まぁいいでしょう!!終わり〜〜

Hugo で構築されています。
テーマ StackJimmy によって設計されています。