QiitaとHexoで同時投稿するには

最初に

これは、あくまで、Qiita over Hexoであることに注意したい。私が、最初にHexoで投稿するようになって、のちに、Qiitaで投稿しようとしたため、ディレクトリ階層などは、Hexoが主軸になる。

HexoとQiitaの概略

どちらもMarkDown形式のファイルを自動化されたcss、javascriptと合わせて、最終的に、htmlに変換するサービスを提供している。npmパッケージリポジトリにそれらをCLIから操作できる便利なツールが出ているため、先に、npm使える状況にないよって人は以下を参考にしてね。
私的ArchLinux開発環境構築 >> nvmによるnpm/node環境構築

Hexo

インストール

1
npm i --save-dev hexo-cli -g

これを実行した後は、どのディレクトリ階層でも、hexoコマンドが使えるようになっていると思う。次に、ディレクトリ階層を作っていくが、それを1から構成するのは骨が折れるので、以下を実行し、必要ファイルをhexoに生成してもらう。

ファイル配置

1
2
# 作業ディレクトリを作ってその中で実行する。
hexo init

記事の作成

1
hexo new "<title>"

これにより、source/_posts/内に<title>.mdが追加されるので、それを編集する。

ローカルサーバー起動(テスト)

規定では、http://localhost:4000で起動される。

1
hexo server

カスタマイズ

デプロイヤー

以下、Githubをデプロイ先として考える。
現状、source/_posts/*.mdファイルを配置し、hexo serverをすることで、ローカルでそれらを確認できるが、実際は、リモートで配信するためのプッシュ(デプロイ)が必要になる。別にGithubのワークフローでも自動プッシュが可能。しかし、私はコマンド派。

1
npm i hexo-deployer-git

_config.ymlの変更場所

1
2
3
4
5
6
# デプロイ先のレポジトリ名が`<user>.github.io`の場合
url: https://<user>.github.io/
deploy:
type: git
repo: https://github.com/<user>/<user>.github.io
branch: master

テーマ

私は、jerryc127/hexo-theme-butterflyを使っている。他にも、テーマはたくさんあるので好みのものを選ぼう。
https://hexo.io/themes/

リンク簡略化〜永久リンクを短くする〜

デフォルトの設定では、投稿後のリンクは、https://<デプロイ先のURL>/2025/04/28/<title>/となるが、以下を導入することで、https://<デプロイ先URL>/posts/8821/というURLでアクセスできるようになる。

1
npm i hexo-abbrlink --save

_config.yml

1
permalink: posts/:abbrlink/

Qiita(Qiita-CLI)

インストール

1
npm i @qiita/qiita-cli --save-dev

これにより、npxコマンドを経由して、qiitaコマンドが使えるようになった。以下、初回に限り実行するもの。

1
2
npx qiita init
npx qiita login

Hexoで作ったmdファイルをQiitaへ投稿するまで

開発での苦難

ここで私は困った。
このqiitaコマンドでは、--root引数により、ルートディレクトリは設定できるものの、publicディレクトリは必要なのだ。
一方、hexoでは、*.mdファイルをsource/_posts/ディレクトリに配置する。ここをうまいこと統合する必要がある。
そこで、私が、その、hexoからqiitaへの橋渡しとなるバッシュスクリプトを開発した。それが以下である。

deploy_to_qiita.shの紹介

deploy_to_qiita.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#!/bin/bash

ROOT_QIITA="qiita"
SOURCE="source/_posts"

cd "$(dirname "$0")"

current_quotepath=$(git config --get core.quotepath)
if [ "$current_quotepath" != "false" ]; then
echo "Setting core.quotepath to false to handle Japanese filenames correctly."
git config --global core.quotepath false
fi

mkdir -p "${ROOT_QIITA}/public"

if [[ $# -gt 0 && $1 == "force" ]]; then
CHANGED_FILES=$(git ls-files "${SOURCE}/*.md")
else
CHANGED_FILES=$(git diff --name-only | grep "${SOURCE}/.*\.md$")
fi

if [ -z "$CHANGED_FILES" ]; then
echo "No .md files changed since last commit."
else
echo "Changed .md files:"
while IFS= read -r file; do
echo "- $file"

source_file="$file"
target_file="${ROOT_QIITA}/public/$(basename "$source_file")"

if [[ -f "$target_file" ]]; then
echo "File exists: $source_file -> $target_file"

temp_file="${target_file}.temp"
mv "$target_file" "$temp_file"

cp "$source_file" "$target_file"

TEMP_FRONT_MATTER=$(head -n 999 "$temp_file" | grep -E "^updated_at:|private:|id:|organization_url_name:|slide:|ignorePublish:")
TEMP_UPDATED_AT=$(echo "$TEMP_FRONT_MATTER" | grep -oE "^updated_at: (.*)$" | sed -E "s/^updated_at: //")
TEMP_PRIVATE=$(echo "$TEMP_FRONT_MATTER" | grep -oE "^private: (.*)$" | sed -E "s/^private: //")
TEMP_ID=$(echo "$TEMP_FRONT_MATTER" | grep -oE "^id: (.*)$" | sed -E "s/^id: //")
TEMP_ORG_URL=$(echo "$TEMP_FRONT_MATTER" | grep -oE "^organization_url_name: (.*)$" | sed -E "s/^organization_url_name: //")
TEMP_SLIDE=$(echo "$TEMP_FRONT_MATTER" | grep -oE "^slide: (.*)$" | sed -E "s/^slide: //")
TEMP_IGNORE=$(echo "$TEMP_FRONT_MATTER" | grep -oE "^ignorePublish: (.*)$" | sed -E "s/^ignorePublish: //")

if [ -n "$TEMP_UPDATED_AT" ]; then
sed -i "1a updated_at: ${TEMP_UPDATED_AT}" "$target_file"
fi
sed -i "2a private: ${TEMP_PRIVATE}" "$target_file"
if [ -n "$TEMP_ID" ]; then
sed -i "3a id: ${TEMP_ID}" "$target_file"
fi
if [ -n "$TEMP_ORG_URL" ]; then
sed -i "4a organization_url_name: ${TEMP_ORG_URL}" "$target_file"
fi
sed -i "5a slide: ${TEMP_SLIDE}" "$target_file"
sed -i "6a ignorePublish: ${TEMP_SLIDE}" "$target_file"

rm "$temp_file"
else
echo "New file: $source_file -> $target_file"

cp "$source_file" "$target_file"
FRONT_MATTER=$(head -n 999 "$target_file" | grep -E "^title:|tags:|abbrlink:|date:")
UPDATED_AT=$(echo "$FRONT_MATTER" | grep -oE "^date: (.*)$" | sed -E "s/^date: //")
sed -i "1a updated_at: \"${UPDATED_AT}\"" "$target_file"
sed -i "2a private: false" "$target_file"
sed -i "3a id: null" "$target_file"
sed -i "4a organization_url_name: null" "$target_file"
sed -i "5a slide: false" "$target_file"
sed -i "6a ignorePublish: false" "$target_file"
fi

md_basename=$(basename "$source_file" .md)
if [ -f "mapping.sh" ]; then
. mapping.sh
each "$ROOT_QIITA/public" "$md_basename"
fi
npx qiita publish --root "${ROOT_QIITA}" "$md_basename"
done <<<"$CHANGED_FILES"
fi

以下、実行時のログ
exec_deploy_to_qiita_sh.png

これにより、Qiitaで投稿する際に必要になる、キーupdated_at;private;id;organization_url_name;slide;ignorePublish;などがhexo new "<title>"コマンドで生成されたsource/_posts/内にある*.mdファイルに対して、自動で追加され、そのまま投稿・更新できるようになる。
なお、一度、qiita/ディレクトリを作り、そこに、source/_posts/内の*.mdファイルをコピーした後で、sedコマンドによるファイル操作を行うので、元の*.mdファイルが汚染されることはない。
最新のdeploy_to_qiita.shについては、以下を参照してほしい。更新があれば、記事の方も更新するようにするので、同じ内容になるかとは思う。(一応)
https://github.com/verazza/blog/blob/master/deploy_to_qiita.sh

deploy_to_qiita.shの簡単な説明と使い方

おおまかな使い方

具体的な使い方を説明しよう。
これは内部で、git diffコマンドによる、source/_posts内の*.mdファイルに差異があれば、それをqiita/publicにコピーして、front-matterを処理するので、git pushする前に、deploy_to_qiita.shを実行する必要がある。

投稿前に文字列置換を行いたい場合

また、もし、qiita投稿時に、特定の*.mdファイルに対して、文字列置換を行いたい場合は、mapping.shdeploy_to_qiita.shと同じ階層に配置すれば、deploy_to_qiita.shが自動で読み込んでくれる。今回、それは、ここには掲載はしないが、興味があれば、以下を見てほしい。
https://github.com/verazza/blog/blob/master/mapping.sh

置換だけ行いたい場合は、以下を実行すれば、source/_posts/内のすべての*.mdファイル(Git管理化にあるもの)に対して、deploy_to_qiita.shを実行できる。

1
./deploy_to_qiita.sh force

今後のdeploy_to_qiita.shをどうするか

にしても、git pushする前に、実行しなければいけないというのは、ユーザーエクスペリエンスの観点で良くないと思う。だから、まずは、これをコミット履歴やもしくはデータベースなどを駆使することで、差異があることを確認するなどして、git diffに依存しない形を取る必要がある。
別途、zennにも投稿できるようにしたい。

同時投稿での注意点

QiitaHexoで同時投稿・同時配信するために、画像リンクなどは、おそらく、Qiitaベースのほうが良い。
理由は、Hexoでの画像表示するときのMARKDOWN形式での書き方は、![](/images/sample.png)というように、ルートに/imagesディレクトリがある前提でpublic/imagesに画像を保存するのだが、Qiitaではそのようなものはサポートされていないためだ。
じゃあ、どうするのかというと、一度、WEBの方で、Qiitaで画像を貼り付けると、https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/から始まる画像リンクが得られるので、これをHexoのソースである*.mdファイルにも貼り付けることで、両方で画像を表示することが可能になる。
めんどうだけどね。
いや、そんなことしなくてもいいのだ。文字列置換があれば、例えば、Qiitaにデプロイするときには、/image/sample.pngという文字列をhttps://qiita-image-store.s3.ap-northeast-1.amazonaws.com/に変換すればいい。すなわち、mapping.shにその旨を書けばよい。

package.jsonのタスク

以下、参考までに。

1
2
3
4
5
6
7
"scripts": {
"server": "hexo clean && hexo generate && hexo server",
"deploy": "hexo clean && hexo generate && hexo deploy",
"deploy2": "./deploy_to_qiita.sh",
"deploy-all": "npm run deploy && npm run deploy2",
"qiita-sync": "npx qiita pull --root qiita"
}

最後に

今回は、記事作成および投稿にhexoを使っていた私がqiitaにも、一応投稿しておくか!ってことで、Qiita-CLIを使いましたが、良い機会になりました。やっぱコマンド触ってるだけでエンジニアっぽい…
追記:
おそらく、これを応用すれば、zennにも投稿できると思うので、またいずれ…

参考

jerryc127/hexo-theme-butterflyテーマを使うときに
https://sj-note.com/hexo-butterfly-upgrade

hexoの有用プラグインの検索に
https://qiita.com/ORCHESTRA_TAPE/items/a0c795904e33cdf043d7