Dec 04, 2016

さくらのレンタルサーバーの一番安いプランでWebサイトを公開するノウハウ

このエントリはさくらのアドベントカレンダー(その2)とのクロスポストです。(→Qiitaの方の投稿

この記事では自分で自由に使えるLinuxなサーバーかPCがあるという事を前提として、さくらのレンタルサーバーのライトプランで静的コンテンツだけのWebサイトを公開・運用する際のノウハウをご紹介します。

なお、自分では調べていませんが、スクリプト内で使用しているlftp等のコマンドがHomebrew等でインストール可能なのであれば、macOS(OS X)でもこの方法をそのまま使えるかもしれません。

SSH接続できない!

自分は日経Linux誌シス管系女子という漫画形式の記事を連載させて頂いているのですが、「せっかく本まで出したんだからプロモーション用のページ作りましょうよ!!絵とかバーンとでっかく貼ってかわいい感じのを!!」と言ってみたものの、「日経BP公式サイトのCMS上ではムリ(大意)」と言われてしまったため、自分で勝手にサイトを作って公開する事にしました。約1年前の事です。

で、そうなるとどこかにサーバを借りる必要があるのですが、自分は個人的に10年来以上のさくらのレンタルサーバーユーザなので、「やっぱ信頼と実績のさくらだよね!」と。

作りたいのはせいぜい1枚ペラの静的コンテンツだったことから、月額にして約129円の一番安いプランで必要十分だろうと判断して、サーバ上の一角をお借りする事にしました。

ところが、ここに大きな誤算がありました。ライトプランではSSH接続は使えないのです……

自分がそれまで個人的に愛用していたスタンダードプランは月額約429円で共用サーバの一角をお借りする物で、こちらはSSHが使えます。そのため、デフォルトのシェルをBashにしたりVimを入れたり、さらにGitを入れたりして、自宅のサーバーに置いたGitリポジトリをgit cloneした物を~/wwwに置き、そのままWebサイトとして公開していました。それと同じ要領で行くつもりだったのですが、月額300円をケチったばかりに目論見が総崩れです。

SSHができなきゃrsyncでお手軽ミラーリングというわけにもいきません。かといって今更Windows用のFTPクライアントを探す気にもなれないし。なので、サイト開設から1年くらいはコントロールパネル上で提供されているWeb UIのファイルマネージャを使ってちまちまファイルをアップロードしていたのですが、シス管系女子 Advent Calendar 2016を立ち上げた結果、1日最低1回はサイトを更新する必要が生じてついに我慢の限界に達しました(ドラッグ&ドロップでファイルをアップロードできなかったり、フォルダまるごと再帰的にアップロードできなかったり、果てはFirefoxの64bit版ではFlashプラグインとの相性が悪いのかエラーでファイルのアップロードすらできなかったり……)。そこで運用開始から1年が経過してようやく、サイトの更新手順の自動化に本腰を入れて取り組む事にしました。

ファイルのアップロードはlftpで一発

静的コンテンツのアップロードは、rsyncを使えればそれが一番楽です。rsync--archiveオプションと--deleteオプションを組み合わせれば、ローカル側で更新されたファイルを転送し、リモート側に取り残されたファイルを削除する、という事(いわゆるミラーリング)を効率よく行えます。が、前述の通りライトプランではこの方法を採れません。

でもよくよく考えてみれば、Linuxのコマンド操作とシェルスクリプトをテーマに解説記事を自分で書いてるわけだから、コマンドラインで使えるFTPクライアントがあればなんとかなりそうです。

そう考えて「rsync FTP」みたいなキーワードで検索してみたら、あっさり見つかりました。その名もlftp。これ自体は対話的に動作するツールとの事ですが、引数で指示を与えれば完全な自動実行も可能で、しかもミラーリングのための便利な内部コマンドを持ってると。rsyncと対でlftp、と名前も覚えやすいですね!(実際にそういう由来で名付けられたのかどうかまでは分かりませんでした。rsyncの初版が公開された前後の時期にlftpという名前になったというのは確かなようですが……)

これを使って、「実行すれば後はrsync感覚で更新されたファイルのコピーと取り残されたファイルの削除が完了する」というシェルスクリプトを用意しました。

upload.sh(まだテスト用):

#!/bin/bash

# 相対パスを気軽に書けるように、
# スクリプトがあるディレクトリにcdすることにする。
root_dir="$(cd "$(dirname "$0")" && pwd)"
cd $root_dir

# ユーザ名が長すぎて「system-admin-gir」で切れてしまってるのはご愛敬……
HOSTNAME=system-admin-gir.sakura.ne.jp
USERNAME=system-admin-gir
PASSWORD=**************
SOURCE=html
DIST=/home/system-admin-gir/www

lftp -d -e "\
  set ftp:ssl-auth TLS; \
  set ftp:ssl-force true; \
  set ftp:ssl-allow yes; \
  set ftp:ssl-protect-list yes; \
  set ftp:ssl-protect-data yes; \
  set ftp:ssl-protect-fxp yes; \
  open -u $USERNAME,$PASSWORD $HOSTNAME ; \
  mirror \
    --reverse \
    --delete \
    --only-newer \
    --dry-run \
    --verbose \
    $SOURCE \
    $DIST; \
  exit"

このスクリプトは、以下のような位置関係でファイルが存在している事を前提にしています。

  • upload.sh
  • html(ディレクトリ)
    • index.html
    • .htaccess
    • ...

lftpコマンドは、実行する内部コマンドを-eオプションで指定すれば非対話的に実行でき、;で区切って複数コマンドを列挙すれば複雑な操作も自動実行できます。この方法を使って、

  1. まずsetでTLSに関わる諸々のオプションを有効化して、セキュアな通信を行うようにする。
  2. その上で、openでログイン。
  3. ログイン後、mirrorでミラーリングを実施。
  4. 終わったらexitで切断。

という操作を行うようにしました。パスワードは平文でスクリプトに書く必要がありますが、TLSを有効化していわゆるFTPSでの通信を確立してから認証するので、パスワードが平文のままネットワークを流れる事はありません。

いきなり実行するのは怖かったので、lftp自体をデバッグモードで動かす-dオプションと、mirrorをテスト実行し詳細情報を出力させるための--dry-run --verboseとを併せて指定しています。この状態でスクリプトを実行してみて、問題がない事を確認できてから改めて--dry-runを取り除くことにします。

共通パーツの埋め込み

前述の通り元々が1枚ペラのページだけに留めるつもりだったため、HTMLファイルには共通のヘッダやフッタ、各SNS向けのシェア用ボタンなどもそのまま静的に記述していました。しかしページ数が増えていくにつれて「これはいつか破綻する……」という危機感が増していき、アドベントカレンダー用に一気に何十ページも増やす事が確定したことでついに破綻が現実化してしまいました。

幸い、ここまでの時点でファイルのアップロードを自動化する事はできました。なのでついでに、各ページのヘッダやフッタといった共通部分を別ファイルに分離しておいて、アップロードの直前にそれらをくっつけてからアップロードするようにしてみます。戦略としては、以下の要領です。

  • build.sh
  • upload.sh
  • html(ディレクトリ、ソース用)
    • _parts(ディレクトリ)
      • metadata.html
      • sharebuttons(ディレクトリ)
        • top.html
      • ...
    • index.html
    • .htaccess
    • ...
  • html_resolved(ディレクトリ、アップロード用)
    • index.html
    • ...

このような位置関係でファイルを置いておき、build.shを実行したらhtmlを丸ごとhtml_resolvedにコピーして、各HTMLファイルの中に

index.html(ソースファイル):

<!DOCTYPE html>
<html lang="ja" xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml">
<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
<!--EMBED(metadata.html)-->
  <title>シス管系女子って何!? - 【シス管系女子】特設サイト</title>
...

のように書かれた箇所を検出して、

metadata.html(共通パーツ):

  <meta charset="UTF-8">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:site" content="@piro_or">
  <meta name="twitter:creator" content="@piro_or">
...

という感じの共通パーツのファイルの内容を

index.html(埋め込み後):

<!DOCTYPE html>
<html lang="ja" xmlns:og="http://ogp.me/ns#" xmlns:fb="http://www.facebook.com/2008/fbml">
<head prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
  <meta charset="UTF-8">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:site" content="@piro_or">
  <meta name="twitter:creator" content="@piro_or">
  ...
  <title>シス管系女子って何!? - 【シス管系女子】特設サイト</title>
...

のように埋め込んでいく、という方針です。

……という説明を見て、昔なつかしSSI(Server Side Include)を思い出した方もいると思います。じゃあSSI使えばええやんという話なのですが、SSIはWebサーバの機能なので、アップロード前の表示確認をするにはローカルにApacheか何かを立てないといけません。それは面倒で嫌だったので、SSIを使わずにそれっぽい事をやることにしたのでした。

できたスクリプトはこんな感じです。

build.sh:共通パーツを埋め込む:

#!/bin/bash

# 相対パスを気軽に書けるように、
# スクリプトがあるディレクトリにcdすることにする。
root_dir="$(cd "$(dirname "$0")" && pwd)"
cd $root_dir

# 環境によってsedで拡張正規表現を使うためのオプションが違うので、
# egrepコマンドのように使える「$esed」を定義しておく。
case $(uname) in
  Darwin|*BSD|CYGWIN*)
    esed="sed -E"
    ;;
  *)
    esed="sed -r"
    ;;
esac

# アップロード用のファイルを用意する。
# とりあえず、前回の結果は削除してソースを丸ごとコピーし直すことにする。
rm -rf html_resolved
cp -r ./html/ ./html_resolved/

# 共通ファイルの埋め込み箇所の検出用正規表現。
# ファイル名部分は、後方参照で取り出せるように`()`で囲っておく。
embed_matcher='<!-- *EMBED\( *([^) ]+) *\) *-->'
# 上記正規表現にマッチした箇所にファイルの内容を埋め込むための
# sedのコマンドライン引数を組み立てるための、sedのスクリプト。
# 詳細は後述。
embed_mark_to_resolver="s|($embed_matcher)| -e '\\\\;\\1;r html/_parts/\\2' -e '\\\\;^ *\\1 *$;d' -e 's; *\\1 *;;'|"

# アップロード用ファイルの中で、ファイル埋め込み用のマークが
# 含まれている物を検索する。
egrep -r \
      --include='*.html' \
      "$embed_matcher" \
      html_resolved |
  cut -d ':' -f 1 | # ファイルパスだけを取り出す。
  uniq | # 1ファイルの中で何カ所も見つかる事があるので、重複を取り除く。
  while read path
do
  # 見つかった各ファイルに対して処理を行う。
  echo "updating $path"
  # 埋め込みマークを埋め込み対象のファイルの内容で置き換えるためのsedのコマンド列を組み立てる。
  # 1. 見つかったファイルの中にあるファイル埋め込み用のマークを抽出して(cat | egrep)、
  # 2. 埋め込みマークをファイルの内容で置き換えるためのsedのコマンド列を組み立てて(sed)、
  # 3. 最後に各行を区切っている改行文字を削除して1行に繋げる(tr)。
  resolve_embedded_resources="sed $(cat "$path" |
                                egrep -o "$embed_matcher" |
                                $esed -e "$embed_mark_to_resolver" |
                                tr -d '\n')"
  # 安全のためファイルを一旦リネームし、
  mv "$path"{,.tmp}
  # sedで一気に内容を解決して、リダイレクトでアップロード用ファイルの位置に書き出す。
  # この時、組み立てたコマンド列にある文字列を括る「'」自体が
  # パラメータの一部として渡されては困るので、
  # evalを使ってコマンド列をシェルの記法で改めてパースさせる。
  cat "$path.tmp" |
    $esed "s/($embed_matcher)( *[^$])/\1"\\$'\n''\3/g' |
    eval "$embed_resolvers" \
    > "$path"
  # 最後に、テンポラリファイルを削除する。
  rm "$path.tmp"
done

sedrコマンド/マッチングパターン/r ファイルのパスと指定すると、そのパターンにマッチした行の後に指定ファイルの中身をそのままはめ込むということができる、というのがポイントです。これで指定ファイルの内容を埋め込んだ後、/マッチングパターン/dで埋め込みマークだけの行を消して、さらにs/マッチングパターン//で行中の埋め込みマークも消す、という風にして痕跡を消しています。(ただしこのままだとマッチングパターンの中にパス区切りの/があった時に破綻するので、それぞれのマッチングパターンを囲むのに/以外の文字を使うように気をつけています。rdについては\;マッチングパターン;dのような要領で明示的に;を指定しています。)

また、sedrはマッチ位置ではなくマッチした行と次の行の間に指定ファイルの内容を出力するため、これだけだと行の中にある埋め込みマークが期待通りに処理されません。そこで、$esed "s/($embed_matcher)( *[^$])/\1"\\$'\n''\3/g'であらかじめ埋め込みマークの直後で強制的に改行するようにしています。

この辺の詳しい事は別の記事にまとめたので、そちらも併せてご覧下さい。

awkを使えばもっとスマートにできるのかもしれませんが、awk使ったら負けかなと思ってる(そのうち何でもawkでやるようになってしまう、というのが怖いのです……あと、シス管系女子の内容の実践という事も意識しているので、なるべく本編で解説した技術だけでやりたいという思いもありました)のでこんな感じになりました。

絶対パスを相対パスに「解決」する

埋め込み用の共通のパーツとして切り出した内容はどの階層のファイルに埋め込まれるか分からないので、共通パーツ内のリンクは相対パスではなく、/から始まる絶対パスで書かないといけません。

しかし、この絶対パスはHTMLファイルがWebサーバからコンテンツとして返されたときにだけ有効な物なので、アップロード前の表示確認をしようと思ってHTMLファイルをブラウザで開くとリンク切れになってしまいます。

先に書いたようにWebサーバを立てたくなかったので、面倒さに目を瞑って今までは相対パスを書いていたのですが、埋め込み用の共通パーツには上記のような理由から相対パスを書けません。そこでこの際だからと、共通パーツの埋め込みと同時に絶対パスを相対パスに解決(って言っていいのか?)するようにしてみました。

build.sh:絶対パスを相対パスに解決する:

...
# リンク先に絶対パスが書かれている箇所の検出用正規表現。
# そのまま残す部分を後方参照で取り出せるように`()`で囲っておく。
absoute_path_matcher="((href|src)=['\"])(/[^/])"

# 埋め込み用の共通パーツを埋め込む前に絶対パスを
# 解決してしまうとまずいので、共通パーツのあるディレクトリは
# 除外して検索するようにする。
egrep -r \
      --include='*.html' \
      --exclude-dir=_parts \
      "$absoute_path_matcher|$embed_matcher" \
      html_resolved |
  cut -d ':' -f 1 |
  uniq |
  while read path
do
  echo "updating $path"
  # ファイルのパスから、最上位のディレクトリへの相対パスにするための
  # 「../../」の部分を作る。
  root="$(echo "$path" |
            $esed -e 's;/[^/]*$;;' \
                  -e 's;[^/]+;..;g' \
                  -e 's;^\.\.;.;' \
                  -e 's;^\./\.\.;..;')"
  # 同じ置換操作を後で2箇所で行うので、sedへ与える指示を
  # ここで変数に格納しておく。
  # (もし置換操作を修正するときはここだけ編集すれば済む)
  updater="s;$absoute_path_matcher;\\1$root\\3;g"
  resolve_embedded_resources="sed $(cat "$path" |
                                egrep -o "$embed_matcher" |
                                $esed -e "$embed_mark_to_resolver" |
                                tr -d '\n')"
  mv "$path"{,.tmp}
  # 最後の書き出し直前に絶対パスを解決する。
  cat "$path.tmp" |
    $esed "s/($embed_matcher)( *[^$])/\1"\\$'\n''\3/g' |
    eval "$embed_resolvers"  |
    $esed -e "$updater" \
    > "$path"
  rm "$path.tmp"
done

絶対パスを相対パスにするということは、言い換えると、そのファイルのパスからドキュメントルートまで到達するのに必要な数だけ../を手前に付け足すということです。この../../の部分を求めるために、grepの結果から得たそのファイル自身のパスをsedで以下のように加工しています。

  1. -e 's;/[^/]*$;;' → html_resolved/path/to/dir/file.htmlの最後の部分を取り除いて、親ディレクトリまでのパスのpath/to/dirにする。
  2. -e 's;[^/]+;..;g' → 各パートを..に置き換えて、../../../..という文字列にする。
  3. -e 's;^\.\.;.;'html_resolvedディレクトリの分だけ余計に上位を指しているので、最初の...に置き換えて./../../..という文字列にする。
  4. -e 's;^\./\.\.;..;' → 先頭の./..は冗長なので、../にする。

3の手順は前述のファイル配置に依存してこうなっているので、違うファイル配置にする場合は適宜読み替えて下さい。

また、絶対パスが書かれた箇所を検出するための正規表現を ((href|src)=['"])(/[^/]) としていますが、これは以下のようなパターンを判別し分ける事を意図しています。

  • href="/path/to/file(サーバ上に置いた時の絶対パスで参照しているパターン) →相対パスに解決する
  • href="path/to/file(すでに相対パスで参照しているパターン) →何もしない
  • href="//hostname/path/to/file(スキーマ部のhttp:を省略したパターン) →何もしない

1番目と3番目を見分ける必要があるので、パスの最初の1文字の/だけでなく、その次の2文字目まで含んだ正規表現としています。

変更が無かったファイルの処理をスキップする

ここまでの手順でアップロード前に共通パーツの埋め込みや絶対パスの解決を行うようにした結果、前処理にそこそこ手間がかかるようになってしまいました。毎回全ファイルに前処理を施しているとキリがありませんので、前処理を施す前のソースファイルに変更があったときだけ前処理をやり直すようにします。

先のスクリプトではアップロード用のファイルを毎回すべて用意し直していましたが、前回用意したファイルに対してrsyncで差分更新を行えば、ソースが更新されたファイルだけ前処理をやり直す事ができるはずです。

build.sh:更新が無かったファイルをスキップする:

...

# rmで前のファイルを消してcpで全コピーする代わりに
# rsyncで必要なファイルだけコピーし直す
mkdir -p html_resolved
rsync --archive --update --delete ./html/ ./html_resolved/

...

rsyncはローカルとリモートの間でのミラーリングに使う事が多いですが、実はこんな風にローカルのファイル同士にも使えるのでした。

--archiveだけだと、ソースと前処理済みのファイルの最終更新日時が「違う」という理由で処理済みファイルが上書きされてしまいます。なので、--updateを明示的に指定して「処理済みファイルの方が新しい場合は何もしない」「処理済みファイルの方が古い場合は、ソースの方が更新されたという事で、前処理をやり直すためにコピーする」という動作になるようにしています。

また、ソースから消えたファイルをアップロード用ファイルから取り除くため--deleteも指定しています。

強制的に前処理をやり直すオプションの追加

先の手順で「変更があったファイルだけを対象に前処理を行う」ようになりました。しかし、前処理の仕方そのものに手を加えたり、埋め込み用のパーツの方に変更を加えた場合には、各ページのファイルの方には変更が無いため前処理がスキップされたままになってしまうという問題があります。一応、html_resolvedを削除すれば強制的に全ファイルに前処理をやり直せますが、画像などのファイルまでアップロードし直す事になってしまうので効率が悪いです。何か良い方法は無いでしょうか?

先程「--archiveだけだと、ソースと前処理済みのファイルの最終更新日時が『違う』という理由で処理済みファイルが上書きされてしまいます(なので--updateも指定する)」と書きました。という事は、--updatersyncに指定しなければ、ここで期待される「前処理済みのファイルだけをソースで上書きして、前処理をやり直す」という事が可能になります。

ということで、rm等のコマンドで一般的な-f(強制実行)オプションに倣って、この前処理用スクリプトにもgetoptsを使って-fオプションを実装してみます。

build.sh:-fオプションが指定されなかったときだけrsync--updateを指定する:

...

# 既定の状態では、`--update`を指定する。
only_update_option='--update'
while getopts f OPT
do
  case $OPT in
    # `-f`が指定された場合、先の変数の値を空文字にする。
    f) only_update_option='';;
  esac
done

mkdir -p html_resolved
# `--update`を直接書く代わりに変数の値を使うようにする。
rsync --archive $only_update_option --delete ./html/ ./html_resolved/

...

もっと効率よくしたい場合には「変更があった埋め込み用パーツを参照しているソースファイルを検出する」という事をする必要があります。今の所は上記のやり方で必要十分なので、これ以上の事をするつもりはありませんが、興味のある方は挑戦してみてはいかがでしょうか。

まとめ

最終的に完成した前処理用のスクリプトとアップロード用のスクリプトは、以下の通りです。

build.sh(完成版):

#!/bin/bash

root_dir="$(cd "$(dirname "$0")" && pwd)"
cd $root_dir

case $(uname) in
  Darwin|*BSD|CYGWIN*)
    esed="sed -E"
    ;;
  *)
    esed="sed -r"
    ;;
esac

only_update_option='--update'
while getopts f OPT
do
  case $OPT in
    f) only_update_option='';;
  esac
done

mkdir -p html_resolved
rsync --archive $only_update_option --delete ./html/ ./html_resolved/

embed_matcher='<!-- *EMBED\( *([^) ]+) *\) *-->'
embed_mark_to_resolver="s|($embed_matcher)| -e '\\\\;\\1;r html/_parts/\\2' -e '\\\\;^ *\\1 *$;d' -e 's; *\\1 *;;'|"
absoute_path_matcher="((href|src)=['\"])(/[^/])"

egrep -r \
      --include='*.html' \
      --exclude-dir=_parts \
      "$absoute_path_matcher|$embed_matcher" \
      html_resolved |
  cut -d ':' -f 1 |
  uniq |
  while read path
do
  echo "updating $path"
  root="$(echo "$path" |
            $esed -e 's;/[^/]*$;;' \
                  -e 's;[^/]+;..;g' \
                  -e 's;^\.\.;.;' \
                  -e 's;^\./\.\.;..;')"
  updater="s;$absoute_path_matcher;\\1$root\\3;g"
  resolve_embedded_resources="sed $(cat "$path" |
                                egrep -o "$embed_matcher" |
                                $esed -e "$embed_mark_to_resolver" |
                                tr -d '\n')"
  mv "$path"{,.tmp}
  cat "$path.tmp" |
    $esed "s/($embed_matcher)( *[^$])/\1"\\$'\n''\3/g' |
    eval "$embed_resolvers"  |
    $esed -e "$updater" \
    > "$path"
  rm "$path.tmp"
done

upload.sh(完成版):

#!/bin/bash

root_dir="$(cd "$(dirname "$0")" && pwd)"
cd $root_dir

# アップロード前に必ず前処理をやり直す。
./build.sh

HOSTNAME=system-admin-gir.sakura.ne.jp
USERNAME=system-admin-gir
PASSWORD=**************
# ソースの方ではなく、前処理済みのファイルの方をアップロードする。
SOURCE=html_resolved
DIST=/home/system-admin-gir/www

lftp -d -e "\
  set ftp:ssl-auth TLS; \
  set ftp:ssl-force true; \
  set ftp:ssl-allow yes; \
  set ftp:ssl-protect-list yes; \
  set ftp:ssl-protect-data yes; \
  set ftp:ssl-protect-fxp yes; \
  open -u $USERNAME,$PASSWORD $HOSTNAME ; \
  mirror \
    --reverse \
    --delete \
    --only-newer \
    --verbose \
    $SOURCE \
    $DIST; \
  exit"

ちゃんとしたCMSを使えばこういう事をわざわざ自分でやる必要も無いのですが、静的なページで済む物を月額129円でミニマムに公開するだけなら、こんな感じで管理の手間を軽減できますよ……という事例のご紹介なのでした。

宣伝:「シス管系女子」について

最後にシス管系女子の宣伝を。Qiitaではなくこちらを読まれている方には「しつこい!」と思われそうですがどうかご容赦ください……

「シス管系女子」は、2011年から日経Linux誌上で連載させて頂いているケーススタディ形式の解説マンガ記事です。

本編では主にSSH接続でLinuxのサーバーにリモートログインして操作する場面を想定して、各種コマンドの使い方を解説しています。またその延長線上として、それらのコマンドの実行手順をシェルスクリプトにして自動処理するという事にも挑戦しています。動作のイメージを絵で表現するように心がけていますので、この記事でやっているような「いろんな手法を組み合わせて、やりたい事を的確に実現する」という事をする上で有用な、丸暗記ではない・より深いレベルでの理解を得られるはずです。

現在は、連載に加筆修正した単行本が以下の2冊リリース済みです。

それ以外にも、Twitterのみんとちゃんbotアカウントイラストや本編に入りきらなかった小ネタを流したり、Webサイトの方にも連載や本では扱わなかったもっと基礎的な話の特別編を置いていたりします。あと、「シス管系女子」をテーマにしたAdvent Calendarも公開中です。

ということで、最後は宣伝で〆てしまいましたがさくらのアドベントカレンダー(その2)の4日目でした。次の方の記事もお楽しみに!

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能