Hidekichi

11tyでDraftを有効にするために

結果的にローカルで見えてリモートで見えなければだいたいokな方法

ドラフトを可能にするために

ドラフト(下書き・草案、設計図等)は完成前の準備段階を指す言葉です。記事は書いてあるが完成してないので非公開にしたいという時、最も確実なのは公開せずローカルのPCの中に置いておくことですが、仮にリモート(Web)サーバーに置いておいても表に見えなければそのアドレスが分からない限り非公開と同等状態になります。11tyのドラフトはこういう感じのものです。

しかし、どうすれば非公開にできるかを考えると、そういう仕組みが11tyにあるわけではありません。それは11tyの作者がそういうシンプルにやるという考え方に基づいていて、

というスタンスでいるからです。おそらくそういう機能も追加できるのでしょうが敢えてシンプルに作られています。現在進行中のバージョン4においてもそういう機能が公式に入るようになるということはありません。11tyの機能としてではなく、ベストプラクティスとしてのコード例として公式のスターターキットなどにも組み込まれています。

個人の自由とは、例えばフロントマターで、

そういうのを邪魔しないということです。自分で「好きに指定」して、「好きに除外」すればいいやんということですね。そのやり方を提示して後は各々でという感じなのです。

どうやって実装するか

最初にしないといけないのは、11tyがどういう状態であるかを判別する事です。それが開発時なのか、ビルド時なのかなどを判別する方法として、

const isServe = process.env.ELEVENTY_RUN_MODE === "serve";

こう宣言しておけば、RUN_MODEがserve、つまり開発時にはisServe = trueになります。開発時以外はfalseですからこれを利用します。

11tyの設定ファイルのeleventy.config.jsに、次のような書き方をします。

export default function (eleventyConfig) {
  // 各種設定
}

例えば、このexport default function (eleventyConfig) {}スコープの外、つまり

const isServe = process.env.ELEVENTY_RUN_MODE === "serve";

export default function (eleventyConfig) {
  // 各種設定
}

このように書けば、グローバルスコープとなってこのファイルの中でどこでもisServeが使えます。スコープの中で使えばローカルスコープとなって、スコープの中でしか使えません。スコープの外でisServeを使うことはあまりないでしょうが、上記のように書いておくのが良いかと思います。

コレクションについて

次に、11tyのどこでも使うコレクション(collection)についてですが、この機能は集めたいデータだけをまとめるものです。blog記事を集めたい場合、

const isServe = process.env.ELEVENTY_RUN_MODE === "serve";

export default function (eleventyConfig) {
  // 各種設定
  
  // コレクション
  eleventyConfig.addCollection("posts", (api) => {
    return api.getFilteredByGlob("src/posts/**/*.md");
  });
}

と言うような感じに書けて、postsという変数に、src/posts/の中にあるMarkdownファイル(記事)を集める(コレクションする)という意味の書き方です。src/posts/の他にsrc/blog/や他のディレクトリからでも集められますし、getFilteredByGlobとあるように、src/{posts,blog}/**/*.mdなどとしても集められます。この時、集められるデータは古いもの順です。

スコープとは{} こういったもので囲まれている部分です。function() {}はカッコ内の処理を実行する関数に当たります。{}の中で定義したものはカッコの中でしか扱えないローカル変数など{}内だけの値になり、{}の外で定義したものはグローバルな値になります。

上記のようにも書けるわけですが、古いもの順なので、ブログの記事などであれば、

return api.getFilteredByGlob("src/posts/**/*.md").reverse();

と書きたくなるでしょう。つまりreverse()を使用して新着順にするということです。新しい記事が先頭に来るという書き方になります。

しかし、公式のドキュメントにも書いてありますが、reverse()は元のデータを逆順に書き換えてしまいます。書き換えられてしまった元々のデータを別で使用した時、並び方が違うなどという問題がでてしまうわけです。1つ2つであれば覚えていられるでしょうが複数利用する場合には元々の状態であった方が間違いがありません。なので、公式ドキュメントでは

を使いましょうとあるわけです。

これらは元々のデータを書き換えずにコピーしてからコピーしたものを逆順にします。なので元のデータは汚染されることがありません。

これらをまず念頭に置いて、draftの件に繋げると、まずdraft記事以外を集めるコードを書いてみます、

eleventyConfig.addCollection("posts", (api) => {
  const posts = api.getFilteredByGlob("src/posts/**/*.md");
  
  if (!isServe) { // RUN_MODE が serve ではない時
    // post.data.draft が trueでないものを返す
    // └> フロントマターに draft: true とあるものを取り除いて返す
    return posts.filter(post => !post.data.draft);
  }
  
  // RUN_MODE が serve の時はposts全部返す
  return posts;
});

このようにも書けます。こういうコレクションは例えばフロントマターについている他のタグでコレクションしたり、日付でコレクションしたりなど色々なコレクションを作ることができますので、そのたび毎にその記事からdraft記事を判定するのは記述も増えますし、reverse()の件もあるので、次のような関数をひとつ作っておきます。

draft記事を取り除いた記事を返す関数

const isServe = process.env.ELEVENTY_RUN_MODE === "serve";

// 11ty設定
export default function (eleventyConfig) {
  // 各種設定
  
  // ----------------------------
  // ドラフト記事を除外する関数
  // ----------------------------
  const noDraft = (items) => {
    return isServe ? [...items] : items.filter(item => !item.data.draft);
  };
 
  // ---------------------------- 
  // コレクション
  // ----------------------------
  
  // 元のコード
  // eleventyConfig.addCollection("posts", (api) => {
  //   return api.getFilteredByGlob("src/posts/**/*.md");
  // });
  
  // 元のコードにnoDraftを適用したコード
  eleventyConfig.addCollection("posts", (api) => {
    return noDraft(api.getFilteredByGlob("src/posts/**/*.md")).reverse();
  });
}

もう答えを全体的に書いてますが、その名もズバリnoDraft()これは.filter()を用いて、集めたコレクションの中からフロントマターにdraft: trueとある記事を除外した新しい配列を作って返します。.filter()は元のデータをコピーしてから操作します。これにより元のデータを汚染せずコピーしたものを加工できるようになります。オリジナルのデータはそのままに、コピーしたデータを操作して返すという便利なメソッドです。操作は、ここでは記事にあるフロントマターのdraft: true以外を(!item.data.draft)という条件で集めて返す関数になっています。

オリジナルの破壊を起こさないので、.reverse()による逆順にして新しいもの順にしても何も問題はありません

説明忘れ

説明忘れてましたがreturn isServe ? trueの時 : falseの時 ;は、isServe(serveの時、つまり開発時)が、

ということです。三項演算子(Ternary operatorまたは、Ternaly)と言います。ローカルで開発時ではドラフト記事が見えてもよいわけで、リモートで公開した場合は見えてはいけないから除外するわけです。いずれにしても.sort()や、.reverse()は使いますからデータの汚染を防ぐことからもコピー後に使用するようにしています。

noDraftisServe ? [...item] とありますが、これはシャローコピーをしています。何から何まで複製するディープコピーの反対で、配列やオブジェクトなどのデータ構造を複製する際、参照のみをコピーして実体の複製は作らない方式のことを言います。なぜこれで良いのかと言うと、つまりデータはこれですよと予め分かれば良いだけなのです。例えば、メモ書きに順番だけを書き写すだけでまるっきり同じものを作り直さなくてもよいというような感じです。順番やどういうものかは必要ですがそのもののクローンである必要はないと言うことです。イメージとしてはOSのファイルのショートカットや、画像編集ソフトのレイヤーの複製みたいなもので実態は元の場所にありそれらがどういう並びであるかをコピーしているだけなのです。

.reverse().sort()は配列自体の並び順を書き換えてしまいますが、そのためだけにファイルのクローンが必要になることはなく、どういう並びでそれがあるかがコピーできればよいわけです。前述したように直接これらのメソッドを使用すると元のデータの並び順を書き換えてしまうから使用はあまり推奨されません。なるべくオリジナルのデータはそのままで他でも問題なく使用できるようにするべきです。であれば、参照しているものが何かがわかるコピーで良く、コピーしたものでそれら(.sort(),.reverse())メソッドを使用すれば、元のデータを破壊せずに済みますし、複製を作る手間(メモリ使用量・CPUパワー)が不要と言うことにもなります。

もっと別で喩えると、GIMPやIllustratorあるいはwebではSVGにおけるUseとか、実体を複製してわざわざ名前を変えて使用しなくても、画像のリンクで配置表示した方が速いでしょう?会社や学校でもマスターデータや書類を持ち歩かず、それをコピーしたものを持ち歩いて必要であれば追記したり何なりとしますよね?マスターに当たる書類に追記はできません。クローンではなくいわゆるただのコピーと考えるとよりわかりやすいかも知れません。

フロントマターとは

---
title: blog記事のタイトル
description: 記事の説明
tags:
   - blog
layout: posts.njk
draft: true
---

## Markdownで記事を書く

記事本文

これまでから何度と無く書いていましたが、こういうMarkdownの先頭にその記事はどういうものかを書いておく部分をフロントマターといいます。上記にも書いてありますが、draft: true部分、これは11tyではitem.data.draftとしてテンプレートやjsで利用できます。

item.data.draftのitemはその時々によって変わります。const posts = ~としてコレクションした場合は、それらをループで一つずつ取り出して、~ => (post ...)となることがあるかと思います。その場合はpost.data.draftとその記事のデータにあるdraftというような意味になります。

最初の方で書いた「ある人は published: false を使うこともあるだろう」という場合は、フロントマターにfalseが設定されているので、publishedがfalse以外は集めるという書き方になります。書き方が混在するとdraft記事を除去するというのが難しくなったような気がしてきますが、いずれにしてもそう言うその記事はまだ下書きであると何かしらが書いてあればtrue|falseの違いはあれど、それはドラフト記事にしたいと言えますから、結果的にはそれらが付いていない記事だけを集めるようにするとシンプルです。そう考えれば、

const noDraft = (items) => {
  return isServe
    ? [...items]
    : items.filter(item =>
        !('draft' in item.data) && !('published' in item.data)
      );
};

こうも書けると思います。私は、フロントマターにdraft: trueと書いているのでオリジナルのnoDraftで問題ないのですが、人間が手動で書く・消すをするため、

---
title: タイトル
draft
---

と値を記載しない場合や、間違った理解から(間違わないと思いますが)、draft: falseと、trueが下書きであるから、falseと書けば公開ってなりそうですが、ちょっとややこしくはないでしょうか?最も簡単なのは、draft部分を不要なら消すが一番シンプルですよね?ドラフトにしたいから付ける、不要なら消すということです。

true|falseでは条件が人間の思考と反対なので条件式がややこしくなります。published: falseだと公開するのはfalseということですが、draft: trueは下書きですと言ってることとなるので、これらは単純には反対の意味を持っています。unpublidhed: trueだと同じかも知れませんが書くのが面倒くさい。

なのでドラフト記事(下書き)としての考え方としては、

こういう考えです。draft :trueをフロントマターに用いて運用すれば、コードは、

const noDraft = (items) => {
  return isServe ? [...items] : items.filter(item => !item.data.draft);
};

と、シンプルかつ美しく書けますから、誰かと記事を書いたりでその書き方が自由な場合、11ty側で吸収するとすれば上記のin等を使用したコードのように工夫は必要ですが、個人なら、

まとめ

Draftにするのはここまでで説明したように設定を書くわけですが、実際の所、記事をローカルに置いておけば良いだけのことではあります。しかしながら何かと操作を間違ったりして公開してしまった時の防波堤としても予め準備はしておくほうが良いです。

.sort().reverse()はオリジナルの配列の並びを変更してしまうのでそれらをコピーしてから使用するのが大事ということです。それぞれのコレクションで別々にDraftに対応するコードを書いても、それが機能していればそれで良いわけですが、それらをひとまとめにするとより便利になると言うことでした。

今回のnoDraft関数を使用せずとも、Node.jsのtoReversed()を使用すれば普通にも書けますし、やり方は人それぞれです。どちらも同じようにコピーしたものを逆順にするということで効率自体もほとんど変わりません。しかしコードを読んだ時にnoDraft(~)とあれば何をしているのかが一目瞭然だろうと思います。Node.jsのtoReversed()を知っていればその意味も自ずと知っているはずですがそれらが明確でない場合でも、そのコードで何をしているのかがわかると良いですよね。

noDraftでコピーしているのに更にtoReversed()をするとまたコピーすることにもなると思うので、それらは避けるようにして下さい。仮にしたとしても問題はないはずですがとにかく後で見て何をしているのかがわかりやすくシンプルで十分な書き方をしているのが良いかと思います。

blogカテゴリ内のタグ一覧