#3 ブログを作った話 - Nuxt.jsでブログを実装する

当ブログ(sumii.io)を、Figma, Nuxt.js, Contentful, Netlifyを使って構築した方法を各パートにわけて紹介していきます。

今回は#1で作成したデザインを元にNuxt.jsでの実装を進めていきます。
ここではNuxt.js(Vue.js)の細かいお作法の説明は割愛し、今回ブログを構築した中で重要なものを抜粋して紹介することにします。

記事一覧のJSONファイルを作る

最終的にはContentfulから記事を投稿し、APIから記事リストのJSONを取得しますが、ContentfulのセットアップとAPIを使えるようにするまでは表示確認用に仮の記事リストのJSONファイルを作っておきましょう。

/** assets/posts.json */
[
  {
    "title": "ほげ,
    "category": "HTML",
    "tags": ["タグ1", "タグ2"],
    "date": "2019-1-30",
    "content": "## タイトル\n\n" + "ほげほげ本文"
    "slug": "post-hoge",
    "id": 1
  },
  {
    "title": "ふが,
    "category": "CSS",
    "tags": ["タグ1", "タグ2"],
    "date": "2019-1-30",
    "content": "## タイトル\n\n" + "ふがふが本文"
    "slug": "post-fuga",
    "id": 2
  }
]

記事カードコンポーネントを作る

記事詳細ページへリンクするカードコンポーネントを作っていきます。propsで渡ってくるデータは記事リストのJSONから取得してくることを想定してください。

Embedded content: https://www.figma.com/file/DijDba1ikJCRyYDaQMRmCznK/sumii.io?node-id=363%3A436

/** components/Card.vue */

<script>
export default {
  props: {
    title: {
      type: String,
      required: true
    },
    category: {
      type: String,
      required: true
    },
    tags: {
      type: Array,
      required: true
    },
    date: {
      type: String,
      required: true
    },
    slug: {
      type: String,
      required: true
    }
  }
}
</script>

<template>
  <article class="card">
    <si-category-icon class="category" :category="category"/>
    <nuxt-link class="post-link" :to="{ name: 'posts-slug', params: { slug: slug } }">
      <time class="date" :datetime="date">{{ format(date, "YYYY/M/D") }}</time>
      <h2 class="title">{{ title }}</h2>
    </nuxt-link>
    <div class="tags">
      <span class="tag" v-for="(tag, i) in tags" :key="i">{{ tag }}</span>
    </div>
  </article>
</template>

<style lang="scss" scoped>…</style>

記事一覧コンポーネントを作る

propsで記事リストを取得することを想定し、v-forでリストの中身を記事カードコンポーネントに渡しています。

Embedded content: https://www.figma.com/file/DijDba1ikJCRyYDaQMRmCznK/sumii.io?node-id=363%3A464

/** components/CardList.vue */

<script>
import Card from "~/components/Card"

export default {
  components: { Card },
  props: {
    posts: {
      type: Array,
      required: true
    }
  }
}
</script>

<template>
  <div class="card-list">
    <card
      class="card"
      v-for="post in posts"
      :title="post.title"
      :category="post.category"
      :tags="post.tags"
      :date="post.date"
      :slug="post.slug"
      :key="post.id"
    />
  </div>
</template>

<style lang="scss" scoped>…</style>

記事コンポーネントを作る

投稿した記事を表示するコンポーネントを作ります。

Embedded content: https://www.figma.com/file/DijDba1ikJCRyYDaQMRmCznK/sumii.io?node-id=363%3A674

sumii.ioはContentfulの管理画面から、Markdown記法を使って記事を投稿しています。
Nuxt.jsでMarkdownをhtmlに変換できるように、markdownitというモジュールを追加しましょう。

yarn add @nuxtjs/markdownit

nuxt.config.jsに以下を追記してください。

/** nuxt.config.js */
module.exports = {
  …
  modules: [ '@nuxtjs/markdownit' ],
  markdownit: {
    injected: true,
    breaks: true,
    html: true
  },
  …
}

これでNuxt.jsでMarkdownをレンダリングすることができるようになりました。
コンポーネントのソースは以下になります。

/** components/PostDetail/PostContent.vue */
<script>
export default {
  props: {
    content: {
      type: String,
      required: true
    }
  }
}
</script>

<template lang="md">
  <div class="post-content v-html="$md.render(content)"/>
</template>

<style lang="scss" scoped>…</style>

propsのcontentで渡ってくるものがmarkdown記法の文字列になります。
<template lang="md">とし、Markdownitで有効になった$md.render()を使用し、markdownをhtmlに変換します。

記事一覧(トップ)ページを作る

記事一覧ページを作成します。
JSONから記事リストを取得し、記事一覧コンポーネントに渡しています。

Embedded content: https://www.figma.com/file/DijDba1ikJCRyYDaQMRmCznK/sumii.io?node-id=452%3A1

/** pages/index.vue */
<script>
import Container from "~/components/Container.vue"
import PageTitle from "~/components/PageTitle.vue"
import CardList from "~/components/CardList.vue"

import posts from "~/assets/posts.json"

export default {
  components: { Container, PageTitle, Posts },
  data() {
    return {
      posts: posts
    }
  }
}
</script>

<template>
  <section>
    <container>
      <page-title title="posts"/>
      <card-list :posts="posts"/>
    </container>
  </section>
</template>

記事詳細ページを作る

Embedded content: https://www.figma.com/file/DijDba1ikJCRyYDaQMRmCznK/sumii.io?node-id=452%3A238

記事詳細ページは渡ってくるパラメータによって表示する記事を出し分けます。
このような動的なルーティングを定義するにはファイルorディレクトリ名にアンダースコアのプレフィックスを付けます。
今回はpages/post/_slug.vueというファイルを作り、判定するパラメータにはslugを使用します。
先程作成した、記事カードコンポーネントを見直すとわかるかと思いますが、nuxt-linkto属性に以下のような記述をし、パラメータにslugを指定して記事詳細ページへ渡しています。

{ name: 'posts-slug', params: { slug: slug } }

そして記事詳細ページでは、渡ってきたパラメータparams.slugを使って記事リストに検索をかけ、ヒットした記事をコンポーネントに渡しています。

/** pages/post/_slug.vue */
<script>
import Container from "~/components/Container.vue"
import PostContent from "~/components/PostConten.vue"

import posts from "~/assets/posts.json"

export default {
  components: { Container, PostContent },
  data() {
    return {
      posts: posts
    }
  },
  computed: {
    post() {
      return this.posts.find(post => post.slug = params.slug)
    }
  }
}
</script>

<template>
  <container>
    <article>
      <!-- header component -->
      <post-content :content="post.content"/>
      <!-- footer component -->
    </article>
  </container>
</template>

<style lang="scss" scoped></style>

最後に

今回はJSONファイルで管理していた記事リストを使いましたが、次回は#4でContentfulのAPIを使って記事リストを取得する方法を紹介します。