Zenn 執筆用リンターを整備する - treefmt で一括実行
はじめに
前回の記事では VSCode と Zenn CLI を利用したローカル執筆環境を紹介しました。
本記事では、リンターの設定構築を紹介します。
具体的には、以下の要素をリンターでチェックします。
- Markdown の体裁(markdownlint-cli2)
- 日本語表現(textlint)
- 英単語のスペル(cspell)
- URL リンク切れ(lychee)
加えて、Zenn に合わせたリンターの設定も紹介します。
また、今回は複数のリンターを用いるので、treefmt でリンターを一括実行する方法も解説します。
想定読者
- Zenn CLI / VSCode で執筆している方
- markdownlint や textlint を使っている(使いたい)方
- 複数のリンターを一括で回したい方
概要
treefmtに各リンターを登録し、一括実行- 対象は
articles/*.mdを指定 - リンターの設定は
linter/に集約
以下の様にリンターでチェックが出来ます。
文末に。がないし空白がある
開かない URL。https://zenn.dev/trifolium/articles/007bff6324743
[存在しないセクションへジャンプ](#a)
[Zenn仕様の画像パス(存在しないファイル)](/images/5b01a68b80808b/hoge.webp)
javascript -> x, JavaScript -> o
JavaScrit タイポ。
- VSCode でリアルタイムにリンターのチェックが入る
URL のチェックだけは CLI 専用にしています

- CLI でリンターを実行
treefmt --config-file ./linter/treefmt.toml
長いので折り畳み
上記の文章を対象にリンターを実行しました。
$ task
ERRO formatter | md[1]: failed to apply with options '[--config linter/.markdownlint-cli2.jsonc]': exit status 1
markdownlint-cli2 v0.18.1 (markdownlint v0.38.0)
Finding: articles/5b01a68b80808b.md
Linting: 1 file(s)
Summary: 2 error(s)
articles/5b01a68b80808b.md:857:14 MD009/no-trailing-spaces Trailing spaces [Expected: 0 or 2; Actual: 1]
articles/5b01a68b80808b.md:860:1 MD051/link-fragments Link fragments should be valid [Context: "[存在しないセクションへジャンプ](#a)"]
ERRO formatter | text[2]: failed to apply with options '[--config linter/.textlintrc.json]': exit status 1
/home/ryu/dev/zenn_contents/articles/5b01a68b80808b.md
857:13 error 文末が"。"で終わっていません。 ja-technical-writing/ja-no-mixed-period
863:1 ✓ error Incorrect term: “javascript”, use “JavaScript” instead terminology
✖ 2 problems (2 errors, 0 warnings, 0 infos)
✓ 1 fixable problem.
Try to run: $ textlint --fix [file]
ERRO formatter | url[3]: failed to apply with options '[--root-dir /home/ryu/dev/zenn_contents --timeout 10 --max-retries 2]': exit status 2
Issues found in 1 input. Find details below.
[articles/5b01a68b80808b.md]:
[ERROR] file:///home/ryu/dev/zenn_contents/images/5b01a68b80808b/hoge.webp | Cannot find file
[404] https://zenn.dev/trifolium/articles/007bff6324743 | Rejected status code (this depends on your "accept" configuration): Not Found
🔍 25 Total (in 0s) ✅ 23 OK 🚫 2 Errors
ERRO formatter | spell[4]: failed to apply with options '[]': exit status 1
1/1 articles/5b01a68b80808b.md 635.59ms X
articles/5b01a68b80808b.md:864:5 - Unknown word (Scrit) fix: (Scrip, Script)
CSpell: Files checked: 1, Issues found: 1 in 1 file.
traversed 164 files
emitted 1 files for processing
formatted 0 files (0 changed) in 4.32s
Error: failed to finalise formatting: formatting failures detected
task: Failed to run task "default": exit status 1
記事の流れ
- 各リンターの紹介
- リンター、
treefmtの導入 - リンターごとの設定
-
treefmtの設定 - VSCode 拡張の設定
フォルダ構成
zenn_contents/
├─ .vscode/
│ ├─ cspell.json // シンボリックリンク
│ ├─ extensions.json
│ └─ settings.json
├─ articles/
├─ node-pkgs/
│ └─ package.json
├─ linter/
│ ├─ treefmt.toml
│ ├─ .markdownlint-cli2.jsonc
│ ├─ .textlintrc.json
│ └─ cspell.json
└─ flake.nix
1. 使用ツールの紹介
1.1 markdownlint-cli2
Markdown の構文チェックを行います。
- CLI
- VSCode 拡張
1.2 textlint
日本語の表記・文体ルールのチェックを行います。
数多くの拡張が公開されており、目的に合わせたカスタマイズが可能です。
- CLI
-
textlint拡張
リンターでチェックしない領域を指定できます。
日本語周りにおけるスペースの有無を決定するルールプリセットです。
技術文書向けのルールプリセットです。
英語の技術文書内の用語、ブランド、テクノロジーのスペルをチェックして修正するためのルールです。
- VSCode 拡張
1.3 cspell
英単語のスペルチェックを行います。
- CLI
- VSCode
1.4 lychee
URL リンク切れのチェックを行います。
- CLI
1.5 treefmt
複数のリンターを一括実行できます。
- CLI
2. 各ツールの導入
「Nix を利用する方法(Zenn CLI 環境として隔離できる、ユーザー環境をクリーンにできるのでお勧め)」と「普通にインストールする方法」を紹介します。
2.1 Nix を利用する方法
2.1.1 前置き
この記事の環境を前提としています。
importNpmLock を用いて、Node パッケージを Nix の devShell で利用可能にします。
詳細はこちらの記事で解説しています。
2.1.2 Node パッケージ管理用のファイルを作成
プロジェクト直下に node-pkgs フォルダを作成し、package.json を作成します。
zenn_contents/
+ ├─ node-pkgs/
+ │ └─ package.json
└─ flake.nix
{
"name": "zenn-cli-env",
"version": "1.0.0",
"private": "true",
"devDependencies": {
"cspell": "9.2.1",
"markdownlint-cli2": "0.18.1",
"textlint": "15.2.2",
"textlint-filter-rule-comments": "1.2.2",
"textlint-rule-preset-ja-spacing": "2.4.3",
"textlint-rule-preset-ja-technical-writing": "12.0.2",
"textlint-rule-terminology": "5.2.15",
"zenn-cli": "0.2.3"
}
}
2.1.3 flake.nix の作成
{
description = "Zenn CLI environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) importNpmLock;
nodejs = pkgs.nodejs_24;
npmRoot = ./node-pkgs;
in
{
devShells.default = pkgs.mkShell {
packages = [
pkgs.treefmt
pkgs.lychee
importNpmLock.hooks.linkNodeModulesHook
];
npmDeps = importNpmLock.buildNodeModules {
inherit npmRoot nodejs;
};
};
# for updating package.json and package-lock.json
devShells.node = pkgs.mkShell {
packages = [
nodejs
pkgs.npm-check-updates
];
};
}
);
}
2.1.4 パッケージの更新
先ほど package.json に記述したパッケージのバージョンが古いかもしれないので、最新へ更新します。
node-pkgs に移動します。
cd your_path/zenn_contents/node-pkgs
devShells.node 環境を利用して、ncu コマンドを実行します。
nix develop .#node -c ncu -u
2.1.5 package-lock.json の作成
npm を利用して package.json に記載された Node パッケージの依存関係を package-lock.json に記述します。
nix develop .#node -c npm install --package-lock-only
2.1.6 devShell の起動
プロジェクト直下に戻り、nix develop で環境を起動します。
package-lock.json に基づいてパッケージが構築され、プロジェクト直下に node_modules がリンクされます。
cd ..
nix develop
これで準備は完了です。
2.2 通常の方法
以下のコマンドを実行してインストールします。
npm install markdownlint-cli2 textlint textlint-filter-rule-comments textlint-rule-preset-ja-spacing textlint-rule-preset-ja-technical-writing textlint-rule-terminology cspell-cli
パッケージ一覧
コピペ用に置いておきます。
markdownlint-cli2
textlint
textlint-filter-rule-comments
textlint-rule-preset-ja-spacing
textlint-rule-preset-ja-technical-writing
textlint-rule-terminology
cspell-cli
snap install lychee
treefmt は GitHub からバイナリをダウンロードできます。
3. 各リンターの設定
3.1 markdownlint-cli2
linter フォルダと .markdownlint-cli2.jsonc を作成します。
zenn_contents/
+ └─ linter/
+ └─ .markdownlint-cli2.jsonc
{
"config": {
"MD001": false,
"MD012": false,
"MD013": false,
"MD022": false,
"MD024": false,
"MD025": false,
"MD029": false,
"MD033": false,
"MD034": false
}
}
ルール一覧(MDxxx)はこちらのページに記載されています。
無効化したルールと理由
MD001: 見出しレベルは一度に 1 レベルずつ増加します
# Heading 1
### Heading 3
# 見出し
#### 目次に表示したくない見出し
こう書きたい時がある。
MD012: 連続する複数の空白行
text
text
# 見出し
解説
# 次の見出し
こう書けた方が編集中に読みやすい。
個人の好み。
MD013: 行の長さ
80 文字を超えると警告が発生する。
textlint と役割が重複しているので、無効化。
MD022: 見出しは空白行で囲む必要があります
# Heading 1
Some text
Some more text
## Heading 2
# 見出し
こう書けた方が読みやすい。
個人の好み。
MD024: 同じ内容の複数の見出し
# Some text
## Some text
# 紹介
## textlint
# 使い方
## textlint
こういった書き方をしたいから。
MD025: 同じ文書内に複数のトップレベル見出しがある
# Top level heading
# Another top-level heading
# はじめに
# 概要
# おわりに
こういった書き方をしたいから。
MD029: 順序付きリスト項目の接頭辞
- 1. まずこれをする
- 2. 次のそれをする
以下の様な書き方を強制するルールです。
1. Do this.
2. Do that.
3. Done.
これだけなら無害だったのですが、NG 例の書き方でも警告が出ました。
NG の様な書き方は使うので無効化。
MD033: インライン HTML
<h1>Inline HTML heading</h1>
|||
|--|--|
|改行無し|改行<br>有り|
表の中で改行する際に HTML タグを利用するから。
MD034: 裸の URL が使用される
For more info, visit https://www.example.com/ or email user@example.com.
https://zenn.dev/zenn/articles/markdown-guide
Zenn 独自の記法にリンクカードがあり、使いたいから。
3.2 textlint
.textlintrc.json を作成します。
zenn_contents/
└─ linter/
├─ .markdownlint-cli2.jsonc
+ └─ .textlintrc.json
{
"plugins": {},
"filters": {
"comments": true
},
"rules": {
"preset-ja-spacing": {
"ja-space-between-half-and-full-width": {
"space": "always"
}
},
"preset-ja-technical-writing": {
"ja-no-weak-phrase": false,
"no-mix-dearu-desumasu": {
"preferInList": "ですます"
},
"ja-no-mixed-period": {
"allowPeriodMarks": [":"]
},
"no-exclamation-question-mark": false
},
"terminology": {
"exclude": [
"V[ -]?S[ -]?Code"
]
}
}
}
追加した拡張
textlint-filter-rule-comments
textlint でチェックしない領域を指定できます。
<!-- textlint-disable -->
This is ignored text by rule.
Disables all rules between comments
<!-- textlint-enable -->
.textlintrc.json では、この拡張を有効化するだけでカスタマイズはしていません。
{
"filters": {
"comments": true
}
}
textlint-rule-preset-ja-spacing
日本語周りにおけるスペースの有無を決定するルールプリセットです。
デフォルトでは、半角・全角の間にスペースを入れない設定になっています。
私の好みで「英単語や半角数字」と日本語はスペースを入れたいので、入れる設定に変更しています。
{
"rules": {
"preset-ja-spacing": {
"ja-space-between-half-and-full-width": {
"space": "always"
}
}
}
}
textlint-rule-preset-ja-technical-writing
技術文書向けのルールプリセットです。
下記をカスタマイズしています。
-
ja-no-weak-phrase
〜と思いますといった弱い表現を禁止します。
そういった表現を使いたいので、許可に変更しています。 -
no-mix-dearu-desumasu
見出しは自動、本文はですます調、箇条書きはである調、で統一するルール。
本文・箇条書きどちらも「ですます調」に統一するように変更しています。 -
ja-no-mixed-period
「。」のつけ忘れチェックを行うルール。
例外として許可したい文字列(「。」として認識する文字列)として「:」を追加しています。
追加しないと、以下の構文で警告が出ます。
:::message
メッセージをここに。
:::
- no-exclamation-question-mark
感嘆符!!、疑問符??を禁止するルール。
使えた方が便利なので許可に変更しています。
{
"rules": {
"preset-ja-technical-writing": {
"ja-no-weak-phrase": false,
"no-mix-dearu-desumasu": {
"preferInList": "ですます"
},
"ja-no-mixed-period": {
"allowPeriodMarks": [":"]
},
"no-exclamation-question-mark": false
}
}
}
textlint-rule-terminology
英語の技術文書内の用語、ブランド、テクノロジーのスペルをチェックして修正するためのルールです。
Javascript → JavaScript
NPM → npm
front-end → frontend
website → site
Internet → internet
この拡張は継続的な設定の見直しが必要だと思います。
例えば、通常だと VSCode はエラーになります。
(Visual Studio Code 表記にすべき)

このエラーを止めたいと仮定して、方法を解説します。
まず、公式の GitHub リポジトリで辞書を確認します。
Visual Studio Code で検索すると、以下が見つかります。
["Visual ?Studio ?Code", "Visual Studio Code"],
["V[ -]?S[ -]?Code", "Visual Studio Code"],
.textlintrc.json の rules.terminology.exclude に V[ -]?S[ -]?Code を追加します。
これで VSCode は許容されます。
なお、Visual ?Studio ?Code は除外指定していないので、visual studio code はエラーが出ます。
{
"rules": {
"terminology": {
"exclude": [
"V[ -]?S[ -]?Code"
]
}
}
}
3.3 cspell
cspell.json を作成します。
zenn_contents/
└─ linter/
├─ .markdownlint-cli2.jsonc
├─ .textlintrc.json
+ └─ cspell.json
{
"version": "0.2",
"files": [
"articles/*.md"
],
"words": [
"Zenn"
]
}
辞書登録
固有名詞は cspell の辞書に登録されていないため、警告がでます。

cspell.json の words に登録したい単語を記述します。
{
"words": [
"Zenn"
]
}
VSCode の場合
クイックフィックスを使うと簡単に辞書登録できます。
Zenn をマウスオーバーして、Add "Zenn" to config: .vscode/cspell.json を押すだけです。


特定の範囲だけ無効化
cspell でチェックしない領域を指定できます。
<!-- cspell:disable -->
This is ignored text by rule.
Disables all rules between comments
<!-- cspell:enable -->
3.4 lychee
lychee.toml を作成します。
zenn_contents/
└─ linter/
├─ .markdownlint-cli2.jsonc
├─ .textlintrc.json
├─ cspell.json
+ └─ lychee.toml
# Maximum number of allowed retries before a link is declared dead.
max_retries = 2
# Website timeout from connect to response finished.
timeout = 10
# Root path to use when checking absolute local links, must be an absolute path
root_dir = "<your_path>/zenn_contents"
root_dir を指定することで、Zenn 独自の画像パスの書き方が正しく認識可能になります。
他の設定はコメントをお読みください。
4. treefmt の設定
treefmt.toml を作成します。
zenn_contents/
└─ linter/
├─ .markdownlint-cli2.jsonc
├─ .textlintrc.json
├─ cspell.json
+ └─ treefmt.toml
[formatter.md]
command = "markdownlint-cli2"
options = ["--config", "linter/.markdownlint-cli2.jsonc"]
includes = ["articles/*.md"]
priority = 1
[formatter.text]
command = "textlint"
options = ["--config", "linter/.textlintrc.json"]
includes = ["articles/*.md"]
priority = 2
[formatter.url]
command = "lychee"
options = ["--config", "linter/lychee.toml"]
includes = ["articles/*.md"]
priority = 3
[formatter.spell]
command = "cspell"
includes = ["articles/*.md"]
priority = 4
[global]
excludes = [
"articles/00341ed49b7935.md"
]
設定の解説
リンターごとの基本的な設定
実際の挙動を見た方が理解しやすいかと思います。
以下の様に設定した場合を考えます。
[formatter.md]
command = "markdownlint-cli2"
options = ["--config", "linter/.markdownlint-cli2.jsonc"]
includes = ["articles/*.md"]
treefmt を実行すると、以下のコマンドを実行したのと同じ結果が得られます。
markdownlint-cli2 --config linter/.markdownlint-cli2.jsonc articles/5b01a68b80808b.md
markdownlint-cli2 --config linter/.markdownlint-cli2.jsonc articles/0a5f92301e3b6f.md
# 以下略
# articles にある全ての .md ファイルを評価する
複数のリンターを使用する
先程の例だと markdownlint-cli のみでしたが、treefmt は複数のリンターを登録できます。
また、リンターを実行する順番は priority で制御可能です。
[formatter.md]
priority = 1
[formatter.text]
priority = 2
[formatter.YourLinterName]
priority = 3
この例だと、対象のファイルに対して md -> text -> YourLinterName の順でリンターが実行されます。
除外設定
treefmt ではチェック対象外のファイルを指定できます。
除外設定は「リンターごと」「リンター全て」どちらも可能です。
[formatter.md]
excludes = ["articles/00341ed49b7935.md"]
[global]
excludes = ["articles/00341ed49b7935.md"]
今回紹介した設定の場合、リンター導入前に書いた記事で警告が鬱陶しいので、global で除外設定をしています。
5. 使用方法
以下を実行すると、リンターによるチェックが行われます。
treefmt --config-file ./linter/treefmt.toml
5.1 動作確認
長いので折り畳み
以下の文章を対象にリンターを実行しました。
文末に。がないし空白がある
開かない URL。https://zenn.dev/trifolium/articles/007bff6324743
[存在しないセクションへジャンプ](#a)
[Zenn仕様の画像パス(存在しないファイル)](/images/5b01a68b80808b/hoge.webp)
javascript -> x, JavaScript -> o
JavaScrit タイポ。
task
ERRO formatter | md[1]: failed to apply with options '[--config linter/.markdownlint-cli2.jsonc]': exit status 1
markdownlint-cli2 v0.18.1 (markdownlint v0.38.0)
Finding: articles/5b01a68b80808b.md
Linting: 1 file(s)
Summary: 2 error(s)
articles/5b01a68b80808b.md:857:14 MD009/no-trailing-spaces Trailing spaces [Expected: 0 or 2; Actual: 1]
articles/5b01a68b80808b.md:860:1 MD051/link-fragments Link fragments should be valid [Context: "[存在しないセクションへジャンプ](#a)"]
ERRO formatter | text[2]: failed to apply with options '[--config linter/.textlintrc.json]': exit status 1
/home/ryu/dev/zenn_contents/articles/5b01a68b80808b.md
857:13 error 文末が"。"で終わっていません。 ja-technical-writing/ja-no-mixed-period
863:1 ✓ error Incorrect term: “javascript”, use “JavaScript” instead terminology
✖ 2 problems (2 errors, 0 warnings, 0 infos)
✓ 1 fixable problem.
Try to run: $ textlint --fix [file]
ERRO formatter | url[3]: failed to apply with options '[--root-dir /home/ryu/dev/zenn_contents --timeout 10 --max-retries 2]': exit status 2
Issues found in 1 input. Find details below.
[articles/5b01a68b80808b.md]:
[ERROR] file:///home/ryu/dev/zenn_contents/images/5b01a68b80808b/hoge.webp | Cannot find file
[404] https://zenn.dev/trifolium/articles/007bff6324743 | Rejected status code (this depends on your "accept" configuration): Not Found
🔍 25 Total (in 0s) ✅ 23 OK 🚫 2 Errors
ERRO formatter | spell[4]: failed to apply with options '[]': exit status 1
1/1 articles/5b01a68b80808b.md 635.59ms X
articles/5b01a68b80808b.md:864:5 - Unknown word (Scrit) fix: (Scrip, Script)
CSpell: Files checked: 1, Issues found: 1 in 1 file.
traversed 164 files
emitted 1 files for processing
formatted 0 files (0 changed) in 4.32s
Error: failed to finalise formatting: formatting failures detected
task: Failed to run task "default": exit status 1
6. VSCode 拡張の設定
6.1 拡張のインストール
markdownlint-cli2、textlint、cspell 用の VSCode 拡張が公開されています。
以下の extensions.json を参考にして、拡張をインストールします。
zenn_contents/
└─ .vscode/
+ └─ extensions.json
{
"recommendations": [
"streetsidesoftware.code-spell-checker",
"davidanson.vscode-markdownlint",
"3w36zj6.textlint"
]
}
6.2 設定
settings.json を作成します。
zenn_contents/
└─ .vscode/
├─ extensions.json
+ └─ settings.json
{
"textlint.configPath": "<your_path>/zenn_contents/linter/.textlintrc.json",
"markdownlint.configFile": "<your_path>/zenn_contents/linter/.markdownlint-cli2.jsonc",
}
設定内容はシンプルで、コンフィグのパスを指定しているだけです。
cspell
Code Spell Checker はコンフィグのパスを指定する設定はありません。
プロジェクト直下、もしくは、.vscode/ にある cspell.json を自動的に読み込む仕様です。
そのため、linter/cspell.json から .vscode/cspell.json へシンボリックリンクを作成します。
以下をプロジェクト直下(zenn_contents)にて実行します。
ln -s ../linter/cspell.json .vscode/cspell.json
zenn_contents/
├─ .vscode/
+ │ ├─ cspell.json // シンボリックリンク
│ ├─ extensions.json
│ └─ settings.json
└─ linter
└─ cspell.json
6.3 動作確認
下画像のように問題タブにリアルタイムでリンターの警告が表示されます。

おわりに
treefmt をハブにすることで、Markdown・日本語・リンク・スペルのチェックを一本化できました。
次回の記事では、Taskfile を利用してコマンド実行を楽にする方法を紹介しようと思います。
Discussion