🗂️
Zenn の記事をフォルダ管理したい
この記事は Zenn の記事を GitHub で管理している方向けの記事です。
記事をフォルダ管理したい
Zenn には GitHub 連携機能が備わっており、自前のエディターで記事を書いてリポジトリに push すると記事を更新できるので、とても便利です。
しかし、articles/ フォルダ直下にあるファイルしか記事判定されないので、増えてくると管理が大変になってきます。
仕様上のフォルダ構造
.
└─ articles/
├── article1.md
├── article2.md
├── article3.md
...
これを以下のように管理したいです。
理想
.
└─ articles/
├── category1/
│ ├── article1.md
│ └── article2.md
├── category2/
│ ├── article3.md
│ ├── article4.md
│ └── article5.md
├── article6.md
...
もちろんこの通りに実現するのは仕様上無理なので、擬似的に実現する方法を紹介します。
実現方法
概ね以下の流れで実現します。
- 記事の本体を別のフォルダ (例えば contents/ ) で作成し、contents/ 以下は自由にフォルダ分けします。
- 記事ごとに設定ファイル (config.yaml) と記事の中身 (名前はなんでも良いが、contents.md とする) を用意します。
- contents.md 監視して、変更があれば自動的に articles/ のファイルを更新する。
変更を検知してリアルタイムに articles/ 以下を更新することで、zenn-cli のプレビュー機能の恩恵を受けられます。
フォルダ構造例
フォルダ構造
.
├── articles/
│ ├── article1.md
│ ├── article2.md
│ ...
└── contents/
├── category1/
│ ├── article1/
│ │ ├── config.yaml
│ │ └── content.md
│ ├── article2/
│ │ ├── config.yaml
│ │ └── content.md
│ ...
├── category2/
...
設定ファイルの仕様
config.yaml の例
この記事のconfig.yaml
front_matter:
title: "Zenn の記事をフォルダ管理したい"
emoji: "🗂️"
type: "idea" # tech: 技術記事 / idea: アイデア
topics: ["Zenn", "記事管理"]
published: false
slug: how_to_manage_zenn_articles
base_path: "contents/zenn/manage_articles"
sections:
- "article.md"
以下の内容を記載します。
-
front_matter
- 記事のタイトル等の情報を記載します。記事の頭に書くやつです。
-
slug
- 記事の slug。artiles/[slug].md が生成されます。
-
base_path
- 監視するファイルのベースのフォルダ
-
sections
- コンテンツファイルの base_path からの相対パス
- 元々記事そのものを分割しようと思って作ったので、複数のファイルを指定できます。単純に上から順に連結します。
front_matter は markdown に含めても良かったですが、published は config で管理すべきだと思ったのでこうしました。
コード
コードを貼っておくので自由に使ってください。
zenn-cli が node なので node で書いた方がいい気がしますが、楽なので python で書きました。
スクリプト
実行方法
python script.py watch [config.yamlのパス]
script.py
from pathlib import Path
from typing import Literal
import click
import watchfiles
import yaml
from pydantic import BaseModel, Field
class FrontMatter(BaseModel):
"""記事の頭のメタ情報"""
title: str = Field(..., description="記事のタイトル")
emoji: str = Field(..., description="絵文字")
type: Literal["tech", "idea"] = Field(
..., description="tech: 技術記事 / idea: アイデア"
)
topics: list[str] = Field(..., description="検索用のタグ")
published: bool = Field(..., description="公開するかどうか")
def get_front_matter_str(self) -> str:
topics_str = ", ".join([f'"{topic}"' for topic in self.topics])
return f"""---
title: "{self.title}"
emoji: "{self.emoji}"
type: "{self.type}" # tech: 技術記事 / idea: アイデア
topics: [{topics_str}]
published: {"true" if self.published else "false"}
---
"""
class ArticleConfig(BaseModel):
slug: str = Field(
...,
description="記事のスラッグ",
min_length=12,
max_length=50,
pattern=r"^[a-z0-9_-]+$",
)
base_path: str = Field(..., description="Base path for the article contents")
sections: list[str] = Field(
..., description="セクション (mdファイル) の base_path からの相対パス"
)
front_matter: FrontMatter = Field(...)
def get_section_paths(self) -> list[Path]:
return [Path(self.base_path) / section for section in self.sections]
def update_article(config: ArticleConfig):
md_str_list = [config.front_matter.get_front_matter_str()]
for section_path in config.get_section_paths():
with open(section_path, "r") as f:
section_str = f.read()
md_str_list.append(section_str)
md_str = "\n".join(md_str_list)
with open(f"articles/{config.slug}.md", "w") as f:
f.write(md_str)
@click.group()
def cli():
pass
@cli.command(help="config.yaml から記事を生成")
@click.argument("conf_yaml_path", type=str)
def make(conf_yaml_path: str):
"""Generate markdown file from config.yaml"""
with open(conf_yaml_path, "r") as f:
conf_dict = yaml.safe_load(f)
conf = ArticleConfig(**conf_dict)
update_article(conf)
@cli.command(help="config.yaml から記事を生成 (監視モード)")
@click.argument("conf_yaml_path", type=str)
def watch(conf_yaml_path: str):
with open(conf_yaml_path, "r") as f:
conf_dict = yaml.safe_load(f)
conf = ArticleConfig(**conf_dict)
update_article(conf)
watch_paths = [conf_yaml_path] + conf.get_section_paths()
print("Watching for changes...")
print("\n".join([conf_yaml_path] + conf.sections))
print("\n")
for changes in watchfiles.watch(*watch_paths):
update_article(conf)
print(f"Updated: {changes}")
if __name__ == "__main__":
cli()
※ config.yaml も監視対象になっていますが、更新しても更新されません。再起動してください。
Discussion