@nuxt/content ✖︎ CompositionAPI で簡素なブログページを作成

## Prologue

これまでアウトプットの下書きは `Vuepress` に書きためていたのですが、`nuxt/content` はコードスニペットもついているしComponent内に埋め込むこともできる、という自由度が気になって基本的な部分を触ってみました。
動作確認を目的としているため、`nuxt-app` で組み込まれるデフォルトのコードベースにいじっているのでその点もご了承ください。
   
基本はDocument通りになるため、自身で設定する場合には都度公式を確認することをお勧めします。

- [nuxt/content](https://content.nuxtjs.org/ja)
- [Nuxt.js](https://ja.nuxtjs.org/)
- [CompositionAPI](https://composition-api.vuejs.org/)

Nuxt.js の初期設定周りは別記事にまとめているので、そちらも参考にしてください。

- [Nuxt.js × TypeScript セットアップ](https://qiita.com/miiiii/items/7d79f4f02303e8e486e3)
- [Nuxt.js × Composition API セットアップ](https://qiita.com/miiiii/items/718409ea0a4d3ce824f0)


## 環境

- macOS: v10.15.5
- node.js: v12.18.2
- terminal: iTerm
- エディタ: VS Code
- パッケージマネージャ: `yarn`
- Composition API: v1.0.0-beta.3

## create app

プロジェクトを作成します。プロジェクト名は今回は `content-sample` としました。

``` Shell
mkdir content-sample
yarn create nuxt-app content-sample

create-nuxt-app v3.1.0
✨  Generating Nuxt.js project in content-sample
? Project name: content-sample
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: Buefy
? Nuxt.js modules: Content
? Linting tools: Prettier
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert
selection)

```

## install

`create nuxt-app` v3.0.0 で `@nuxt/content` が選択できるようになったようです。
今回は新規にプロジェクトを作っているため、それを選択しました。上記の `Nuxt.js modules: Content` がその項目です。
参考: https://github.com/nuxt/create-nuxt-app/releases

`content/` ディレクトリも自動で生成されますが、既存のプロジェクトに追加する際には以下の手順で行ってください。

1. `@nuxt/content` をインストール

```
yarn add @nuxt/content
```

2\. `nuxt.config.js` に以下を追加

```JS
modules:[
    '@nuxt/content'
]
```

3\. `tsconfig.json` に以下を追加

``` JS
"types: [
    "@nuxt/content"
]
```


## マークダウンを表示する

今回は `content/hello.md` が予め用意されていたので、その md ファイルを利用して表示ができるかをまず確認します。
※ UI Framework に Buefy を選択しているので、既存のコードベースに進めていきます。

既存の `pages/index.vue` を以下のように修正します。
参考: https://content.nuxtjs.org/ja/fetching


``` JS
<template>
  <section class="section">
   // 略
    <nuxt-content :document="doc" />
  </section>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from '@vue/composition-api'
import Card from '@/components/Card.vue'

export default defineComponent({
  name: 'HomePage',
  components: {
    Card,
  },
  setup(props, {root}) {
    const doc = ref<Object>({})
    // @ts-ignore
    const ctx = root.context
    onMounted(async () => {
      doc.value = await ctx.$content('hello').fetch();
    })

    return { 
      doc
    }
  },
})
</script>

```

### コードの説明

グローバルにアクセスできる `this.$content` を利用して、コンテンツを取得します。

- `$content(path, options?)`
`path` にはファイルかディレクトリを指定できるので、今回は `hello.md` の `hello` を指定し、`fetch()` をつなげて Promise を返してもらいます。
`path` が ファイルなら Object, ディレクトリなら `Array` のため、今回は返り値を格納する変数 `doc` を Object型で作成しました。

- `<nuxt-content> component`
Markdown コンテンツの body を表示するために必ず使用します。props は `document`　です。

`hello.md` の内容が表示されたらOKです。


## param 付きのページを作成

`articles/article_1` という構成のページを作成して、`BASE_URL/articles/article_1` で表示ができることを目指します。

1. `content/articles/article_1` を作成

``` JS
## date: 2020-07-12

I get a new PC!!
```

2\. `pages/articles/_slug.vue` を作成

``` JS
<template>
  <section class="section">
    <nuxt-content :document="article" />
  </section>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from '@vue/composition-api';
import Card from '@/components/Card.vue';

export default defineComponent({
  name: 'HomePage',
  components: {
    Card,
  },
  setup(props, context) {
    const article = ref<Object>({});

    // @ts-ignore
    const ctx = context.root.context;
    onMounted(async () => {
      article.value = await ctx.$content('articles', context.root.$route.params.slug).fetch();
    })
    return {
      article
    }
  }
});
</script>
```

### コードの説明

`$content('article', params.slug)` の形式で `/article/${params.slug}` になるとのことなので、
`pages` に同じroute構成になるようディレクトリとファイルを作成し、表示できるようコードを作成しました。


## YAML フロントマターブロックのタイトルを、ページタイトルとして取得、表示

1. `content/articles/article_1` にYAML形式でタイトルを追加

``` JS
---
title: shopping
---

## date: 2020-07-12

I get a new PC!!
```

2\. `pages/articles/_slug.vue` も修正

``` JS
<template>
  <section class="section">
    {{titleName}}
    <nuxt-content :document="article" />
  </section>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from '@vue/composition-api';
import Card from '@/components/Card.vue';

export default defineComponent({
  name: 'HomePage',
  components: {
    Card,
  },
  setup(props, context) {
    const article = ref<Object>({});
    const titleName = ref<string>('');

    // @ts-ignore
    const ctx = context.root.context;
    onMounted(async () => {
      article.value = await ctx.$content('articles', context.root.$route.params.slug).fetch();
      const { title } = await ctx.$content('articles', context.root.$route.params.slug).only(['title']).fetch();
        titleName.value = title;
    })
    return {
      article,
      titleName
    }
  },
});
</script>
```

### コードの説明

`const {title}` 以下で `title` のパラメータを取得し、それを表示用の変数 `titleName` に格納しました。
分割代入をしているので、`const {title}` に使う変数名はキーと揃えています。
今回は動作確認のためにこういう書き方をしましたが、先に取得した `article.value.title` からも取得することができます。

参考: 分割代入 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

## ページを増やす

`content/articles/` 下に article_2.md, article_3.md ... と増やしていき、URLを変えていくとその内容が表示されます。
一つレイアウトを作っておけば記事が挿し変わるので、特に実装の変更はありません。

## 表紙を作成

`pages/articles/index.vue` を作成し、`articles` 下のMDファイルを全て表示します。

``` JS
<template>
  <section class="section">
    <div v-for="(article, index) in articles" :key="index" class="article-content">
      <a :href="article.path">
        <h2>{{article.title}}</h2>
      </a>
      <nuxt-content :document="article" />
      <hr />
    </div>
  </section>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from '@vue/composition-api';
import Card from '@/components/Card.vue';
import { Context } from '@nuxt/types';

export default defineComponent({
  name: 'Articles',
  components: {
    Card,
  },
  setup(props, context) {
    const articles = ref<Array<any>>([]);
    // @ts-ignore
    const ctx = context.root.context;
    onMounted(async () => {
      articles.value = await ctx.$content('articles').fetch();
    })
    return {
      articles
    }
  },
});
</script>

<style lang="scss">
.article-content:not(:last-child) {
  margin-bottom: 2rem;
}
</style>

```

- コードの説明

`$content` の引数にディレクトリ名を渡して、配列で取得し、それを `for` で回します。
タイトルは各オブジェクト内にあるので(key: `title`)それも合わせて表示し、さらにオブジェクト内に `path` があるのでリンクを貼ります。

## 目次を作成

目次は自動で作られる、とドキュメントにありましたが、自動...どこに...
見つけられなかったのとプルダウン形式で表示させたかったため、自力で実装しました。
コードは割愛しますが、表紙を作成した際と同じやり方で、`path` と `title` を取得しそれをループで表示させます。

以上です。
記事の目次、本文を割と簡単に実装できました。この他にも query 検索や、 tag で記事をカテゴリ別に作成したり、などもできるようなので次はそれを実装してみようと思います。

## Epilogue

今回詰まったところは `$content` の取得と param付きページの表示です。CompositionAPIを使おうと思っていたので、表示ができなかった際にどちらが原因なのか、問題の切り分けに時間がかかりました。
SSRでの実装もやっていみたいので、それも時間があればまた記事にします。

表記に関してなど何かありましたらご連絡ください。