Hakyllでブログ作成
1 はじめに
講義資料の公開,置き場やら自分のプロフィール載せるために取り敢えずブログを作った.
取り敢えずHaskell静的サイトでググって最初に出てきたHakyllを使うことにした.
Haklly開発者のjaspervdjのブログのソースコードをほとんどそのまま使っている ため,今後色々変えていく予定.
取り敢えずの変更点として,
- ↑はcabalで開発していたので,stackにした
- Hakyll+KaTeXで数式を書けるようにした
- シンタックスハイライトの追加
- 目次の追加
- safari対応
- 細かなデザインの変更
など. ソースコードはこちら. 今後なにか変更を加えたら書いていく.
2 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
3 KaTeX
基本的にはこちらのサイトを参考にした2015年の記事で現在はHakyllを使っておらず,ソースコードが消えていたので,補うのに苦労した. KaTeXの情報が古かったので,mathCtxを最新のKaTeXのテンプレにした.
mathCtx :: Context a
= field "katex" $ \item -> do
mathCtx <- getMetadataField (itemIdentifier item) "katex"
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)に追加する.
"lectures/*.md" .||. "lectures/*.html" .||. "lectures/*.lhs") $ do
match ($ setExtension ".html"
route $ pandocCompiler
compile >>= 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$<meta property="og:description" content="$description$" />$endif$
$if(description)$</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.writerExtensions
にPandoc.enableExtension Pandoc.Ext_tex_math_dollars
を指定する. Ext_tex_math_double_backslash
は\( .. \)
など.
-- Custom WriterOptions: disable `$...$` math, enable fenced_divs, plus TOC etc.
customWriterOptions :: Pandoc.WriterOptions
= defaultHakyllWriterOptions
customWriterOptions = Pandoc.KaTeX ""
{ Pandoc.writerHTMLMathMethod = Pandoc.enableExtension Pandoc.Ext_fenced_divs
, Pandoc.writerExtensions $ 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
}
"lectures/*.md" .||. "lectures/*.html" .||. "lectures/*.lhs") $ do
match ($ setExtension ".html"
route $ pandocCompilerWith customReaderOptions customWriterOptions
compile >>= 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") {
.render( texText.data
katex, mathElements[i]
, { displayMode: mathElements[i].classList.contains("display")
, throwOnError: true }
;
)
}
};
})</script>
</body>
4 シンタクスハイライトの変更
シンタックスハイライトを変更した シンタックスは,pandocCompilerのオプションとして指定できる.
import Text.Pandoc.Highlighting
pandocCodeStyle :: Style
= breezeDark
pandocCodeStyle customWriterOptions:: Pandoc.WriterOptions
= defaultHakyllWriterOptions { Pandoc.writerHTMLMathMethod = Pandoc.MathJax ""
customWriterOptions = Just pandocCodeStyle}
, Pandoc.writerHighlightStyle
myPandocCompiler :: Compiler (Item String)
=
myPandocCompiler pandocCompilerWith defaultHakyllReaderOptions customWriterOptions
このように適用することでこのブログの見た目になっている.
"posts/*.md" .||. "posts/*.html" .||. "posts/*.lhs") $ do
match ($ setExtension ".html"
route $ myPandocCompiler
compile >>= saveSnapshot "content"
>>= return . fmap demoteHeaders
>>= loadAndApplyTemplate "templates/post.html" (postCtx tags)
>>= loadAndApplyTemplate "templates/content.html" (mathCtx <> defaultContext)
>>= loadAndApplyTemplate "templates/default.html" (mathCtx <> defaultContext)
>>= relativizeUrls
5 目次の生成
こちらのサイトそのままで導入した. だんだんPandocのOptionが長くなっていく…
import Prelude
import Data.Functor.Identity (runIdentity)
= either error Prelude.id . runIdentity . Pandoc.compileTemplate "" $ T.unlines
tocTemplate "<div class=\"toc\"><div class=\"header\">Table of Contents</div>"
[ "$toc$"
, "</div>"
, "$body$"
,
]
customWriterOptions:: Pandoc.WriterOptions
= defaultHakyllWriterOptions
customWriterOptions = Pandoc.MathJax "" -- LaTeX
{ Pandoc.writerHTMLMathMethod = True
, Pandoc.writerNumberSections = Just pandocCodeStyle -- Syntax
, Pandoc.writerHighlightStyle = True -- toc
, Pandoc.writerTableOfContents = 3
, Pandoc.writerTOCDepth = Just tocTemplate
, Pandoc.writerTemplate }
として,
"posts/*.md" .||. "posts/*.html" .||. "posts/*.lhs") $ do
match ($ setExtension ".html"
route $ do
compile <- getUnderlying
underlying <- getMetadataField underlying "tableOfContents"
toc 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
6 sitemapの生成
postCtxを修正して, 修正時間(mtime
),url(url
)を追加.
postCtx :: Tags -> Context String
= mconcat
postCtx tags "mtime" "%Y-%m-%d"
[ modificationTimeField "date" "%Y-%m-%d"
, dateField "tags" tags
, tagsField "url"
, urlField Context $ \key -> case key of
, "title" -> unContext (mapContext escapeHtml defaultContext) key
-> unContext mempty key
_
, defaultContext ]
templates
にsitemap.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> </
create
でsitemap
を生成.
"sitemap.xml"] $ do
create [
route idRoute$ do
compile <- loadAllSnapshots "posts/*" "content"
posts <- loadAllSnapshots "lectures/*" "content"
lectures let allPosts = posts ++ lectures
let sitemapCtx = constField "root" "https://yakagika.github.io" <>
"entries" (postCtx tags) (return allPosts)
listField
""
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> </
7 複数のタグを付ける
lectures と post の2つからタグを生成する.
複数のPattern
からTags
を生成するためのbuildTagsWithList
を定義して.
=> [Pattern] -> (String -> Identifier) -> m Tags
buildTagsWithList :: MonadMetadata m = do
buildTagsWithList patterns makeId <- concat <$> mapM getMatches patterns
ids <- foldM addTags M.empty ids
tagMap set' = S.fromList ids
let return $ Tags (M.toList tagMap) makeId (PatternDependency (mconcat patterns) set')
where
-- Create a tag map for one page
id' = do
addTags tagMap tags <- getTags id'
' = M.fromList $ zip tags $ repeat [id']
let tagMapreturn $ M.unionWith (++) tagMap tagMap'
以下のように使う.
-- Build tags
<- buildTagsWithList ["posts/*","lectures/*"] (fromCapture "tags/*.html") tags
markdown
のメタデータに以下のように設定すると,ちゃんと動く.
---
title: 特別講義(データサイエンス)
description: 資料
tags:
- datascience
- statistics
- python
featured: true
tableOfContents: true
---
yakagika