Blog

Hakyllでブログ作成

最初の投稿. Hakyllでブログを作成するあれこれ.

はじめに

講義資料の公開,置き場やら自分のプロフィール載せるために取り敢えずブログを作った.

取り敢えずHaskell静的サイトでググって最初に出てきたHakyllを使うことにした.

Haklly開発者のjaspervdjのブログソースコードをほとんどそのまま使っている ため,今後色々変えていく予定.

取り敢えずの変更点として,

など. ソースコードはこちら. 今後なにか変更を加えたら書いていく.

stack

package.yamlを追加したのみ. これで,

stack build

stack exec main build (2回目以降はrebuild)

stack exec main watch

で確認できる. (執筆段階では,まだローカルで試しているだけ) package.yamlは以下.

ependencies:
  - base
  - binary
  - directory
  - filepath
  - hakyll
  - pandoc
  - process
  - text
  - containers

library:
  source-dirs: src

_exe-defs: &exe-defaults
  dependencies: blog


executables:
  main:
    <<: *exe-defaults
    main:                Main.hs
    source-dirs:         src

KaTeX

基本的にはこちらのサイトを参考にした2015年の記事で現在はHakyllを使っておらず,ソースコードが消えていたので,補うのに苦労した. KaTeXの情報が古かったので,mathCtxを最新のKaTeXのテンプレにした.

mathCtx :: Context a
mathCtx = field "katex" $ \item -> do
    katex <- getMetadataField (itemIdentifier item) "katex"
    return $ case katex of
                    Just "false" -> ""
                    Just "off" -> ""
                    _ -> "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css\" integrity=\"sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww\" crossorigin=\"anonymous\">\n\
                             \<script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js\" integrity=\"sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd\" crossorigin=\"anonymous\"></script>\n\
                             \<script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js\" integrity=\"sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk\" crossorigin=\"anonymous\" onload=\"renderMathInElement(document.body);\"></script>"

これでコンテクストを作って,mappend (<>)でdefaultContext (template/default.html)に追加する.

match ("lectures/*.md" .||. "lectures/*.html" .||. "lectures/*.lhs") $ do
        route   $ setExtension ".html"
        compile $ pandocCompiler
                >>= saveSnapshot "content"
                >>= return . fmap demoteHeaders
                >>= loadAndApplyTemplate "templates/lecture.html" (postCtx tags)
                >>= loadAndApplyTemplate "templates/content.html" (mathCtx <> defaultContext )
                >>= loadAndApplyTemplate "templates/default.html" (mathCtx <> defaultContext)
                >>= relativizeUrls

もとのブログは,markdownのメタデータにおいて, katex : trueとなっているものだけにKaTeXを適用するのが方針で, 以下のように書くことで実現できる. “$$”で数式が書けるようにdelimitersを設定する必要があるのだが,Hakyllの仕様で,“$”が消えるので全部二重にしたら上手く行った.

<!-- 旧版 -->
<!DOCTYPE html>
<html lang="en" $if(dark)$class="dark"$endif$>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">

        <title>yakagika - $title$</title>

        <!-- Stylesheets. -->
        <link rel="stylesheet" type="text/css" href="/style.css?v=0">

        <!-- RSS. -->
        <link rel="alternate" type="application/rss+xml" title="yakagika" >
        <!-- href="http://jaspervdj.be/rss.xml" -->

        <!-- Metadata. -->
        <meta name="keywords" content="yakagika Haskell ExchangeAlgebra">
        <meta name="description" content="Personal home page and blog of yakagika.">
        $if(katex)$
        <!-- KaTeXのスタイルシートとJavaScriptのリンクを動的に挿入 -->
        $katex$
        $endif$
        $if(description)$<meta property="og:description" content="$description$" />$endif$
    </head>
    <body>
        <div id ="navigation">
            <h1>Contents</h1>
            <a href="/">Home</a>
            <a href="/posts.html">Blog</a>
            <a href="/lectures.html">Lecture</a>
            <a href="/research.html">Research</a>
            <a href="/contact.html">Contact</a>
            <!-- <a href="/cv.html">CV</a> -->
            <h1>Links</h1>
            <a href="http://github.com/yakagika">GitHub</a>
        </div>

        $body$
        <!-- GUID -->
        <div style="display: none">ce0f13b2-4a83-4c1c-b2b9-b6d18f4ee6d2</div>
        $if(katex)$
        <!-- KaTeX JavaScript and auto-render extension -->
        <script>
          document.addEventListener("DOMContentLoaded", function() {
            renderMathInElement(document.body, {
              delimiters: [
                {left: "$$$$", right: "$$$$", display: true},
                {left: "$$", right: "$$", display: false} // インライン数式用のデリミタを追加
              ]
            });
          });
        </script>
        $endif$
    </body>
</html>

とりあえずこんな感じで出せる.

あいうえお \\( f(あ) = a^2 \\) かきくけこ

あいうえお $ f(あ) = a^2 $ かきくけこ

$$ f(x) = \frac{1}{x}  $$

あいうえお \( f(あ) = a^2 \) かきくけこ

あいうえお $ f(あ) = a^2 $ かきくけこ

f(x) = \frac{1}{x}

(2025/03/27修正) –

上の方法だと,Pandoc時点で数式処理されてmath inlineとなった要素とJavaScript側で処理されてlatex.inllineとなった要素が混在して,ところどころデザインが崩れるので,Pandocの時点でKaTeXを使用するように変更した.

結構苦労したので追記.

writerOption において Pandoc.writerHTMLMathMethod = Pandoc.KaTeX "" と設定することでKaTeXで数式が処理される. $ ... $で式を示したい場合は, Pandoc.writerExtensionsPandoc.enableExtension Pandoc.Ext_tex_math_dollarsを指定する. Ext_tex_math_double_backslash\( .. \)など.

-- Custom WriterOptions: disable `$...$` math, enable fenced_divs, plus TOC etc.
customWriterOptions :: Pandoc.WriterOptions
customWriterOptions = defaultHakyllWriterOptions
  { Pandoc.writerHTMLMathMethod  = Pandoc.KaTeX ""
  , Pandoc.writerExtensions      = Pandoc.enableExtension Pandoc.Ext_fenced_divs
                                 $ Pandoc.enableExtension Pandoc.Ext_tex_math_dollars
                                 $ Pandoc.enableExtension Pandoc.Ext_tex_math_double_backslash
                                 $ Pandoc.enableExtension Pandoc.Ext_tex_math_single_backslash
                                 $ Pandoc.pandocExtensions
  }

match ("lectures/*.md" .||. "lectures/*.html" .||. "lectures/*.lhs") $ do
        route   $ setExtension ".html"
        compile $ pandocCompilerWith customReaderOptions customWriterOptions
                >>= saveSnapshot "content"
                >>= return . fmap demoteHeaders
                >>= loadAndApplyTemplate "templates/lecture.html" (postCtx tags)
                >>= loadAndApplyTemplate "templates/content.html" (mathCtx <> defaultContext )
                >>= loadAndApplyTemplate "templates/default.html" (mathCtx <> defaultContext)
                >>= relativizeUrls

これで

$x=1$

$$
x = 1
$$

と書かれているmdから


<span class="math inline"> $x=1$ </span>

<span class="math display"> $x=1$ </span>

のような形のhtmlに変換される. これをdefault.html側でレンダリングする.レンダリングは以下でOK. (cf.https://github.com/jaspervdj/hakyll/issues/1006#issuecomment-2369250865)

<head>
$if(katex)$
    $katex$
$endif$
</head>

<body>
 <script>
    document.addEventListener("DOMContentLoaded", function () {
      var mathElements = document.querySelectorAll('.math');
      for (var i = 0; i < mathElements.length; i++) {
        var texText = mathElements[i].firstChild
        if (mathElements[i].tagName == "SPAN") {
            katex.render( texText.data
                        , mathElements[i]
                        , { displayMode: mathElements[i].classList.contains("display")
                          , throwOnError: true }
                        );
          }
      }
    });
  </script>
</body>

シンタクスハイライトの変更

シンタックスハイライトを変更した シンタックスは,pandocCompilerのオプションとして指定できる.

import Text.Pandoc.Highlighting
pandocCodeStyle :: Style
pandocCodeStyle = breezeDark
customWriterOptions:: Pandoc.WriterOptions
customWriterOptions = defaultHakyllWriterOptions { Pandoc.writerHTMLMathMethod = Pandoc.MathJax ""
                                                 , Pandoc.writerHighlightStyle = Just pandocCodeStyle}

myPandocCompiler :: Compiler (Item String)
myPandocCompiler =
    pandocCompilerWith defaultHakyllReaderOptions customWriterOptions

このように適用することでこのブログの見た目になっている.

 match ("posts/*.md" .||. "posts/*.html" .||. "posts/*.lhs") $ do
        route   $ setExtension ".html"
        compile $ myPandocCompiler
                >>= saveSnapshot "content"
                >>= return . fmap demoteHeaders
                >>= loadAndApplyTemplate "templates/post.html" (postCtx tags)
                >>= loadAndApplyTemplate "templates/content.html" (mathCtx <> defaultContext)
                >>= loadAndApplyTemplate "templates/default.html" (mathCtx <> defaultContext)
                >>= relativizeUrls

目次の生成

こちらのサイトそのままで導入した. だんだんPandocのOptionが長くなっていく…

import           Prelude
import           Data.Functor.Identity (runIdentity)

tocTemplate = either error Prelude.id . runIdentity . Pandoc.compileTemplate "" $ T.unlines
  [ "<div class=\"toc\"><div class=\"header\">Table of Contents</div>"
  , "$toc$"
  , "</div>"
  , "$body$"
  ]

customWriterOptions:: Pandoc.WriterOptions
customWriterOptions = defaultHakyllWriterOptions
                        { Pandoc.writerHTMLMathMethod   = Pandoc.MathJax ""       -- LaTeX
                        , Pandoc.writerNumberSections   = True
                        , Pandoc.writerHighlightStyle   = Just pandocCodeStyle    -- Syntax
                        , Pandoc.writerTableOfContents  = True                    -- toc
                        , Pandoc.writerTOCDepth         = 3
                        , Pandoc.writerTemplate          = Just tocTemplate
                        }

として,

    match ("posts/*.md" .||. "posts/*.html" .||. "posts/*.lhs") $ do
        route   $ setExtension ".html"
        compile $ do
                underlying <- getUnderlying
                toc        <- getMetadataField underlying "tableOfContents"
                let writerOptions' = maybe defaultHakyllWriterOptions (const customWriterOptions) toc

                pandocCompilerWith defaultHakyllReaderOptions writerOptions'
                >>= saveSnapshot "content"
                >>= return . fmap demoteHeaders
                >>= loadAndApplyTemplate "templates/post.html" (postCtx tags)
                >>= loadAndApplyTemplate "templates/content.html" (mathCtx <> defaultContext)
                >>= loadAndApplyTemplate "templates/default.html" (mathCtx <> defaultContext)
                >>= relativizeUrls

sitemapの生成

postCtxを修正して, 修正時間(mtime),url(url)を追加.

postCtx :: Tags -> Context String
postCtx tags = mconcat
    [ modificationTimeField "mtime" "%Y-%m-%d"
    , dateField "date" "%Y-%m-%d"
    , tagsField "tags" tags
    , urlField "url"
    , Context $ \key -> case key of
        "title" -> unContext (mapContext escapeHtml defaultContext) key
        _       -> unContext mempty key
    , defaultContext
    ]

templatessitemap.xmlを追加.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    $for(entries)$
    <url>
        <loc>$url$</loc>
        <lastmod>$mtime$</lastmod>
    </url>
    $endfor$
</urlset>

createsitemapを生成.

    create ["sitemap.xml"] $ do
        route idRoute
        compile $ do
            posts <- loadAllSnapshots "posts/*" "content"
            lectures <- loadAllSnapshots "lectures/*" "content"
            let allPosts = posts ++ lectures
            let sitemapCtx = constField "root" "https://yakagika.github.io" <>
                             listField "entries" (postCtx tags) (return allPosts)

            makeItem ""
                >>= loadAndApplyTemplate "templates/sitemap.xml" sitemapCtx
                >>= relativizeUrls

以下のようなサイトマップが生成される.

<?xml version="1.0" encoding="UTF-8" ?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">

    <url>
        <loc>/posts/2024-03-29-a-first-post.html</loc>
        <lastmod>2024-06-12</lastmod>
    </url>

    <url>
        <loc>/lectures/2024-03-29-introduction-to-algebraic-programing.html</loc>
        <lastmod>2024-06-12</lastmod>
    </url>

    <url>
        <loc>/lectures/2024-03-29-introduction-to-statistics.html</loc>
        <lastmod>2024-06-12</lastmod>
    </url>

    <url>
        <loc>/lectures/2024-03-29-special-lecture-datascience-answer.html</loc>
        <lastmod>2024-06-12</lastmod>
    </url>

    <url>
        <loc>/lectures/2024-03-29-special-lecture-datascience.html</loc>
        <lastmod>2024-06-12</lastmod>
    </url>

</urlset>

複数のタグを付ける

lectures と post の2つからタグを生成する.

複数のPatternからTagsを生成するためのbuildTagsWithListを定義して.

buildTagsWithList :: MonadMetadata m => [Pattern] -> (String -> Identifier) -> m Tags
buildTagsWithList patterns makeId = do
    ids <- concat <$> mapM getMatches patterns
    tagMap <- foldM addTags M.empty ids
    let set' = S.fromList ids
    return $ Tags (M.toList tagMap) makeId (PatternDependency (mconcat patterns) set')
  where
    -- Create a tag map for one page
    addTags tagMap id' = do
        tags <- getTags id'
        let tagMap' = M.fromList $ zip tags $ repeat [id']
        return $ M.unionWith (++) tagMap tagMap'

以下のように使う.

-- Build tags
tags <- buildTagsWithList ["posts/*","lectures/*"] (fromCapture "tags/*.html")

markdownのメタデータに以下のように設定すると,ちゃんと動く.

---
title: 特別講義(データサイエンス)
description: 資料
tags:
    - datascience
    - statistics
    - python
featured: true
tableOfContents: true
---

yakagika

ce0f13b2-4a83-4c1c-b2b9-b6d18f4ee6d2