NixでTypstをBuildしGitHub Pagesでホスティングする

Table of Contents

背景

スライドや組版記事を生成する為にTypstを採用することにした。 工数を最小限に安定的に量産できるようなしくみを整える必要が迫られていたので、今回対応したことをメモしておく。

ソースコードはtakeokunn/blogのtypstディレクトリ下に置いている。

条件

  • MUST
    • Localに一切依存しない再現性の高いBuild環境構築
    • org-modeで記述できる
    • Typstのエコシステムを使う
    • スライド生成と組版記事両方に対応する
  • SHOULD
    • takeokunn.orgから配信できる
    • リリースまでのリードタイムが短い
    • org-roamなど周辺のエコシステムとの親和性を高める

方針

以下の3ステップで実現した。

  • org-modeで記述したファイルをTypstに変換
  • NixでTypstをBuildできるようにする
  • GitHub ActionsでBuildしてGitHub Pagesで配信できる

作業ステップ

0. どこに置くか検討

以下を理由に takeokunn/blog 上に構築することにした。

  • takeokunn.org から配信できる
  • 既存のorg-roam上に相乗りできる
  • 新しいリポジトリを作ると関心ことが増える
  • すでにあるGitHub Actions上に構築することによって実装コストとメンテナンスコストを抑えられる
  • 今回の用途だと作業途中でもパブリックにして問題がない

商業誌のような公開制限をかける必要のあるものは都度プライベートリポジトリを作って対応することにした。

1. org-modeで記述したファイルをTypstに変換

組版記事の場合、jmpunkt/ox-typst を用いて通常通りOrgファイルをTypstに変換する。(M-x org-typst-export-to-typst を実行)

PHPerKaigi2025のパンフ記事の場合、以下のようにTypstの設定をExportしている。

#+BEGIN_EXPORT typst
#set text(lang: "ja", font: "Migu", size: 8pt)

#set page(
  width: 210mm,
  height: 297mm,
  margin: 20mm,
  columns: 1
)

#import "@preview/codly:1.2.0": *
#import "@preview/codly-languages:0.1.1": *
#show: codly-init.with()
#codly(languages: codly-languages)

#align(center)[
  #set text(size: 18pt)
  Phpactorから学ぶLanguage Server Protocolの仕組み

  #set text(size: 12pt)
  たけてぃ \@takeokunn
]
#+END_EXPORT

スライドの場合、 Orgファイルには登壇メモをしつつ、 #+BEGIN_EXPORT typst のみ出力してほしかったので以下のようなElispを書いた。

(require 'ox-typst)

(setq org-export-with-toc nil)

(org-export-define-backend 'typst-slide
  '((export-block . org-typst-export-block)
    (headline . org-typst-headline)
    (item . org-typst-item)
    (keyword . org-typst-keyword)
    (section . org-typst-section)
    (src-block . org-typst-src-block))
  :menu-entry
  '(?y "Export to Typst"
       ((?F "As Typst buffer" org-typst-export-as-typst)
        (?f "As Typst file" org-typst-export-to-typst)
        (?p "As PDF file" org-typst-export-to-pdf)))
  :options-alist
  '((:typst-format-drawer-function nil nil #'(lambda (_ contents) contents))
    (:typst-format-inlinetask-function nil
                                       nil
                                       #'(lambda (_ contents) contents))))

(defun org-typst-slide-export-as-typst (&optional async subtreep visible-only body-only ext-plist)
  (interactive)
  (org-export-to-buffer 'typst-slide "*Org Typst Slide Export*"
    async subtreep visible-only body-only ext-plist))

(defun org-typst-slide-export-to-typst (&optional async subtreep visible-only body-only ext-plist)
  (interactive)
  (let ((outfile (org-export-output-file-name ".typ" subtreep)))
    (org-export-to-file 'typst-slide outfile
      async subtreep visible-only body-only ext-plist)))

2. NixでTypstをBuildできるようにする

組版記事とスライドの場合で実行したいElisp関数が違うので、引数に type を渡すことで条件分岐をした。 Nix経由でインストールしたものを TYPST_FONT_PATHS TYPST_PACKAGE_PATH でPATHを通して typst compile を実行するDerivationを作った。

output抜粋:

buildTypstProject = { name, type }:
  let
    _ = assert builtins.elem; type [ "article" "slide" ];
    emacsBuildPhase = name: if type == "article"
                            then
                              "emacs --batch --load ox-typst.el --file ${name}/article.org --funcall org-typst-export-to-typst"
                            else
                              "emacs --batch --load ox-typst.el --file ${name}/article.org --funcall org-typst-slide-export-to-typst";
  in
    pkgs.stdenv.mkDerivation {
      inherit name;
      src = ./.;
      nativeBuildInputs = with pkgs; [
        typst
        migu
        (emacs.pkgs.withPackages (epkgs: with epkgs; [ org ox-typst ]))
      ];
      buildPhase = ''
        ${emacsBuildPhase name}
        export TYPST_FONT_PATHS="${pkgs.migu}/share/fonts/truetype/migu"
        export TYPST_PACKAGE_PATH="${typstPackagesCache}/typst/packages"
        typst compile ${name}/article.typ
      '';
      installPhase = ''
        mkdir -p $out
        cp ${name}/article.pdf $out/${name}.pdf
      '';
    };

呼び出し方はシンプルで、以下のように packages.* で定義するとBuildできるようになった。

packages = {
  example-slide = buildTypstProject {
    name = "example-slide";
    type = "slide";
  };
  phperkaigi-2025-pamphlet = buildTypstProject {
    name = "phperkaigi-2025-pamphlet";
    type = "article";
  };
};

#import "@preview/codly:1.2.0": * のようにインポート記述のみすると、Nix Sandbox環境だとうまくインストールできなかった。(参考: Typixを使って複数環境でtypstでスライドをコンパイルする - Zenn)

inputsに typst-packages を定義してPATHを通すとうまくBuildできた。 TypstのNixラッパである loqusion/typix のコードも読んだが、自分の用途だと自前で書けば良いという結論に至ったので採用しなかった。

inputs抜粋:

inputs = {
  typst-packages = {
    url = "github:typst/packages";
    flake = false;
  };
};

3. GitHub ActionsでBuildしてGitHub Pagesで配信できる

Hugoのデプロイフローの最後に nix build して生成したPDFを public/pdf/ にコピーする処理を追加した。 https://github.com/takeokunn/blog/blob/main/.github/workflows/main.yml

- name: Generate example-slide
  run: |
    nix build ./typst#example-slide
    cp result/example-slide.pdf public/pdf/    
- name: Generate phperkaigi-2025-pamphlet
  run: |
    nix build ./typst#phperkaigi-2025-pamphlet
    cp result/phperkaigi-2025-pamphlet.pdf public/pdf/    

生成されたPDFは以下。

Next Step

安定的にBuildできるようになったので、Typst自体の記述に慣れつつスライドや記事を量産していきたい。 また、現状Miguフォントを使っているが個人的には納得していなく、テーブル表示にするとなぜかずれてしまう問題が発生している。 nixpkgs内にある日本語フォント選定に時間を割きたい。

雑感

当初掲げていた条件をすべて満たせたので満足。 Typixを使って複数環境でtypstでスライドをコンパイルする - Zenn 記事に助けられたのでOmochiceに大感謝。