microCMS

Nuxt×microCMS×Netlifyでポートフォリオを作ってみよう

しょうみゆ

この記事は公開後、1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、しょうみゆです。
今回はNuxt.jsとmicroCMSでJamstack構成のWebエンジニア向けポートフォリオを作成していこうと思います。
ポートフォリオはブログよりも考慮すべき点が少なくハードルが低いので、Jamstackに興味があってこれから始めたい方にも優しい内容となっております。

今回完成したものがこちらです。
APIを2つ使用してメインビジュアル、プロフィール、制作物を管理画面から管理できるようにしています。
https://confident-brattain-1276f1.netlify.app/

また、デザインはこのようにモノクロシンプルで作成していますので、慣れてきたらお好きな色やレイアウトに変更してみてください。


では、早速作成していきましょう!

microCMSの準備


サービスの作成


まずはmicroCMSでサービスを作成しましょう。
下記のように作成しましたが、みなさんはお好きなIDに変更してください。



サービス画像はスキップし、プラン選択はHobbyとしました。

APIの作成


続いてAPIを作成しましょう。
API名とエンドポイントは下記のように設定しました。



APIの型は【リスト形式】にして、ステップ3でAPIスキーマを定義していきます。

APIスキーマにはポートフォリオとして載せたい項目を登録していきます。
私の場合は下記の内容で設定しましたが、必要に応じて項目を増やしたり減らしたりしてみてください。
(APIスキーマは後から変更が可能です。)


『担当』『技術』『ツール』の3つは複数選択可能のセレクトフィールドとして、下記のように設定しています。
こちらも増減は後から可能ですので、お好きな内容で登録しましょう。
(コンテンツ参照にしても良いですがプランによって作成できるAPIの数に制限があるので、今回はセレクトフィールドで対応しています。)

担当


技術


ツール


一通り入力ができたら、完了ボタンを押しましょう。

制作物を登録する


worksのAPIができたので、実際にコンテンツを登録しましょう。
画像やテキストは一旦ダミーで大丈夫ですが、フロントでの開発を考慮して長文で入力したり、実際に近い内容で入力することをお勧めします。

入力が完了したら公開ボタンを押して、APIで取得できる状態にしておきましょう。

Nuxtプロジェクトの準備


プロジェクトの作成


microCMSの準備はできたので、Nuxtプロジェクトを作成していきましょう。
yarnを使用してインストールします。(npmを使用する方は適宜変更してご対応ください)

$ yarn create nuxt-app portfolio


インストール中に対話形式で質問されるので下記のように進めました。



しばらくするとインストールが完了します。

Sassのインストール


Sassを使用して効率よく開発していきます。
公式ドキュメントを参考に、必要なパッケージをインストールしましょう。

$ yarn add --dev sass sass-loader@10


これでvueファイルのstyleタグ内でSassを使用できるようになりました。

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


Sass変数と関数を作成する


これから使用するSass変数や関数を作成しましょう。

まず、変数にはよく使用するようなカラーコードやフォントなどを定義していきます。
デザインがとてもシンプルなので定義する内容も少ないですが、よく使用するような値があればここで定義することによって今後の運用が楽になりますので積極的に使っていきましょう。
色を変えたい場合にはこのファイルでカラーコードを変えれば全体に適用されるように設計しています。

プロジェクト直下にassetsのディレクトリを作成して、assets/scss/settings/_variables.scssに下記のように入力します。

// -------------------------------------------
// Color Settings
// -------------------------------------------
// base
$base-color-primary: #fff;
$base-color-secondary: #fafafa;

// text color
$text-color-primary: #010101;
$text-color-secondary: #fff;

// key
$key-color-black: #010101;

// background
$color-body-background: $base-color-primary;

// -------------------------------------------
// Typeface Settings
// -------------------------------------------
$font-base: 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', 'Osaka',
  'メイリオ', 'Meiryo', 'MS Pゴシック', 'MS P Gothic', sans-serif;

// -------------------------------------------
// Break Points
// -------------------------------------------
$breakpoints: (
  'sm': 480px,
  'md': 768px,
  'lg': 1024px,
  'xl': 1200px,
  'xxl': 1440px,
) !default;


次にfont-sizeを指定した数字でremに変換する関数を定義しましょう。
assets/scss/settings/_functions.scssに下記のように入力します。

@use 'sass:math';

/**
 * 引数のfontSizeをremに変換する関数
 * @param fontSize フォントサイズ
 */
@function fz($fontSize) {
  @return math.div($fontSize, 16) * 1rem;
}


この関数はfont-size: fz(24);のように使うと24px相当でremに変換してくれる関数です。font-sizeの指定が楽になりますのでよければ使ってみてください。

次にmixinにメディアクエリの定義をしていきます。
assets/scss/settings/_mixins.scssに下記のように入力します。

@use 'sass:map';
@use 'variables' as var;
//============================================
//  MIXIN
//============================================
// Media Queries
@mixin mq($breakpoint: md, $type: min) {
  @if map.has-key(var.$breakpoints, $breakpoint) {
    @if ($type == max) {
      $width: map.get(var.$breakpoints, $breakpoint);
      $width: $width - 1;
      @media screen and (max-width: #{$width}) {
        @content;
      }
    } @else if($type == min) {
      @media screen and (min-width: #{map.get(var.$breakpoints, $breakpoint)}) {
        @content;
      }
    }
  }
}


上記の設定で@include mq() { ... }のように使用することができます。

最後に作成したこの3つのファイルをimportしてひとつにまとめたファイルを作成しましょう。
assets/scss/app.scssを作成して下記のように入力します。パスに注意してください。

@import './settings/variables';
@import './settings/functions';
@import './settings/mixins';


Sass変数や関数をグローバル化する


今回インストールしたSassはDart Sassなので、変数を使用する際にはvueファイル内で毎回@use文を使用して変数ファイルを指定する必要があります。

<style lang="scss" scoped>
@use "variables" as var;
.text {
  color: var.$base-text-color;
}
</style>


変数を使用する度に読み込むのは面倒なので、グローバル化するために@nuxtjs/style-resourcesをインストールします。

$ yarn add -D @nuxtjs/style-resources


nuxt.config.jsのbuildModulesセクションに先ほど作成したapp.scssを読み込ませます。

export default {
  buildModules: [
    '@nuxtjs/style-resources', // 追加
  ],

  styleResources: {
    scss: ['~/assets/scss/app.scss'],
    hoistUseStatements: true,
  },
}


これでどのファイルからでも@use文なしで変数、関数、mixinが呼び出せるようになりました。

ベーススタイルの設定


リセットCSSとベースになるCSSを作成しましょう。
リセットCSSにはressを使用します。

$ yarn add ress


nuxt.config.jsで読み込ませましょう。
CSSセクションでは、グローバルで使用するCSSやSassを指定することができます。

export default {
  css: ['ress'],
}


次にベーススタイルを定義しましょう。
assets/scss/base.scssに下記のように入力します。(いつも使用している設定があればそれを使用してもOKです。)

html {
  box-sizing: border-box;
  height: 100%;
}

*,
*::before,
*::after {
  box-sizing: inherit;
}

body {
  height: 100%;
  font-family: $font-base;
  font-size: fz(16);
  line-height: 1.5;
  color: $text-color-primary;
  letter-spacing: 0.05em;
  background-color: $color-body-background;
}

img {
  max-width: 100%;
  height: auto;
  vertical-align: bottom;
}

a {
  color: inherit;
  text-decoration: none;
}

h1,
h2,
h3,
h4,
h5,
strong {
  font-weight: bold;
}

input,
textarea {
  max-width: 100%;
  font-family: inherit;
  font-size: 100%;
}


先ほど設定した変数や関数も使用しています。
これもnuxt.config.jsで読み込ませましょう。

export default {
  css: ['ress', '~/assets/scss/base.scss'],
}


グローバルスタイルの設定


ベーススタイル同様にファイルを作り、指定します。
このファイルにはサイト全体で使用する共通スタイルを定義していきます。今回はひとつのファイルにまとめていますが、用途に合わせてファイルを分割するとより運用しやすい構成にできると思います。
/assets/scss/global.scssを作成して下記を追加しましょう。

.visuallyHidden {
  position: absolute;
  top: 0;
  left: 0;
  white-space: nowrap;
  width: 1px;
  height: 1px;
  overflow: hidden;
  border: 0;
  padding: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  margin: -1px;
}

.container {
  max-width: 780px;
  width: 90%;
  margin-left: auto;
  margin-right: auto;

  &--lg {
    max-width: 960px;
  }
}

.row {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
}

.child {
  padding-bottom: 5em;
}

.background--gray {
  background-color: $base-color-secondary;
}

.sectionPrimary {
  padding: 5em 0;

  @include mq() {
    padding: 7.5em 0;
  }
}

.headingPrimary {
  font-family: $font-ubuntu;
  font-size: fz(40);
  font-weight: bold;
  text-transform: capitalize;
  text-align: center;
  margin-bottom: 1em;
}

.button-area {
  text-align: center;
  margin-top: 2em;

  @include mq() {
    margin-top: 2.5em;
  }
}

.buttonPrimary {
  color: $key-color-black;
  display: inline-block;
  font-family: $font-ubuntu;
  font-size: fz(18);
  font-weight: bold;
  text-transform: capitalize;
  text-align: center;
  text-indent: -1em;
  line-height: 56px;
  padding: 0 1em;
  border: 2px solid;
  border-radius: 4px;
  min-width: 230px;
  position: relative;

  &::after {
    content: '';
    display: inline-block;
    width: 24px;
    height: 24px;
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTkuNzUgNy41MDI0NEwxNSAxMi4wMDAyTDkuNzUgMTYuNTAyNCIgc3Ryb2tlPSIjMDEwMTAxIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=)
      center no-repeat;
    background-size: contain;
    position: absolute;
    top: 50%;
    right: 1em;
    transform: translateY(-50%);
    transition: all 0.3s ease-in-out;
  }

  &:hover {
    &::after {
      transform: translate(5px, -50%);
    }
  }

  &--leftArrow {
    text-indent: 1em;

    &::after {
      background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0LjI1IDcuNTAyNDRMOSAxMi4wMDAyTDE0LjI1IDE2LjUwMjQiIHN0cm9rZT0iIzAxMDEwMSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K);
      background-size: contain;
      right: inherit;
      left: 1em;
    }

    &:hover {
      &::after {
        transform: translate(-5px, -50%);
      }
    }
  }
}


こちらもnuxt.config.jsで読み込ませましょう。

export default {
  css: ['ress', '~/assets/scss/base.scss', '~/assets/scss/global.scss'],
}


Google Fontsの読み込み


英見出しにGoogle FontsのUbuntuを使用しています。
headで読み込ませても良いですが、Nuxtではローダーが用意されているのでそれを使いましょう。

$ yarn add nuxt-webfontloader


nuxt.config.jsに下記のように追加しましょう。

export default {
  modules: [
    'nuxt-webfontloader',
  ],

  webfontloader: {
    google: {
      families: ['Ubuntu:wght@400,700&display=swap'],
    },
  },
}


フォントを変数として使えるようにしておきましょう。
assets/scss/settings/_variables.scss に追加します。

$font-ubuntu: 'Ubuntu', sans-serif;


これでようやくコーディングの準備が整いました。

ヘッダーとフッターを作成する


先に共通パーツとなるヘッダーを作成します。
componentsディレクトリにTheHeader.vueを作成して下記のように入力しましょう。

components/TheHeader.vue

<template>
  <header class="header">
    <div class="headerContainer">
      <!-- ロゴ -->
      <component :is="isTopPage ? 'h1' : 'p'" class="logo">
        <nuxt-link to="/">My Portfolio</nuxt-link>
      </component>

      <!-- メニュー -->
      <nav>
        <ul class="menu">
          <li>
            <nuxt-link to="/#about" class="menu__link">about</nuxt-link>
          </li>
          <li>
            <nuxt-link to="/works" class="menu__link">works</nuxt-link>
          </li>
        </ul>
      </nav>
    </div>
  </header>
</template>

<script>
export default {
  computed: {
    // TOPページかどうか
    isTopPage() {
      if (this.$route.name === 'index') return true
      return false
    },
  },
}
</script>

<style lang="scss" scoped>
.header {
  width: 100%;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  background-color: $base-color-primary;
}

.headerContainer {
  padding: 0 1.5em;
  display: flex;
  justify-content: space-between;
  align-items: center;

  @include mq() {
    padding: 0 4em;
  }
}

.logo {
  color: $text-color-primary;
  font-family: $font-ubuntu;
  font-size: fz(18);
  font-weight: bold;
  letter-spacing: 0;

  @include mq() {
    font-size: fz(24);
  }
}

.menu {
  list-style: none;
  display: flex;
  margin-right: -0.75em;

  &__link {
    font-family: $font-ubuntu;
    font-weight: bold;
    text-transform: capitalize;
    line-height: 64px;
    display: inline-block;
    padding: 0 0.25em;
    position: relative;

    @include mq() {
      padding: 0 0.75em;
    }

    &::after {
      content: '';
      display: block;
      width: 0;
      height: 2px;
      background-color: $key-color-black;
      transition: all 0.3s ease-in-out;
      position: absolute;
      left: 0;
      bottom: 0;
    }

    &:hover {
      &::after {
        width: 100%;
      }
    }
  }
}
</style>


ヘッダーロゴはTOPページと下層ページでh1とpタグを使い分けたかったので<component>を使用し、関数で出し分けています。
内部へのリンクはnuxt-linkを、外部サイトへのリンクはaタグを、と使い分けることでサイト内での高速なページ遷移を実現できます。

続いてフッターを作ります。
components/TheFooter.vueに下記のように入力しましょう。

<template>
  <footer class="footer">
    <p class="copyright">
      <small>©️2021 My Portfolio All Rights Reserved.</small>
    </p>
  </footer>
</template>

<style lang="scss" scoped>
.footer {
  padding: 1em 0;
  background-color: $base-color-secondary;
}

.copyright {
  font-size: fz(14);
  font-family: $font-ubuntu;
  text-align: center;
}
</style>



作成したヘッダーとフッターはサイト全体で使用したいので、layout/default.vueを作成して、下記のように差し込みます。
layouts/default.vue

<template>
  <div class="wrapper">
    <TheHeader class="header" />
    <main class="main">
      <Nuxt />
    </main>
    <TheFooter />
  </div>
</template>

<style lang="scss" scoped>
.wrapper {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.header {
  position: fixed;
  z-index: 100;
}

.main {
  flex: 1;
  overflow-x: hidden;
  padding-top: 4em;
}
</style>


<Nuxt />の部分にpages配下で作成したページの内容が表示されます。

nuxt.config.jsでcomponents: trueが指定されているので、ヘッダー・フッターコンポーネントをimportせずに使用できるようになっています。Nuxtのバージョンによってはこの指定がない場合もあるので、なければ自分で設定してみてください。

もし下記のようにeslintのエラーが出たら、

error  Component name "default" should always be multi-word  vue/multi-word-component-names


.eslintrc.jsのrulesに以下を追加すると解消されます。修正したらdevサーバーを再起動してください。

module.exports = {
  rules: {
    'vue/multi-word-component-names': 'off', // 追加
  },
}


ここまでの完成イメージです。


このようにサイト全体で使用するパーツは、コンポーネントとしてlayoutsに挿入することで使用可能になります。

各ページのコーディング


ここからは、microCMSのAPIを流し込む想定で各ページをコーディングしていきます。

TOPページ


/pages/index.vueに下記のように入力して保存しましょう。

<template>
  <div>
    <div class="mainVisual">
      <picture>
        <source
          srcset="https://placehold.jp/375x530.png"
          media="(max-width: 767px)"
        />
        <img src="https://placehold.jp/1440x436.png" alt="" />
      </picture>
    </div>

    <section id="about" class="sectionPrimary">
      <div class="container">
        <h2 class="headingPrimary">about</h2>
        <div class="profile">
          <div class="profile__upper">
            <div class="profile__text">
              <p class="profile__name">
                山田 太郎<span lang="en">Taro Yamada</span>
              </p>
              <dl class="profile__item">
                <dt class="profile__title">技術スタック</dt>
                <dd>HTML / CSS / jQuery / JavaScript / Nuxt</dd>
              </dl>
              <dl class="profile__item">
                <dt class="profile__title">趣味</dt>
                <dd>開発、ゲーム、YouTube、ライブ、音楽フェス、ピアノ、卓球</dd>
              </dl>
            </div>
            <figure class="profile__image">
              <img src="https://placehold.jp/260x260.png" alt="your name" />
            </figure>
          </div>
          <p class="profile__message">
            自己紹介を入れましょう。出身や経歴と現在の仕事の内容を簡単に話すも良し。<br />数年後の目標や今学んでいること、活動している内容を入れるのも良いかと思います。
          </p>
        </div>
      </div>
    </section>

    <section class="sectionPrimary background--gray">
      <div class="container">
        <h2 class="headingPrimary">works</h2>
        <ol class="row works">
          <li class="works__item">
            <nuxt-link to="#!" class="works__inner">
              <figure class="works__image">
                <img src="https://placehold.jp/370x229.png" alt="" />
              </figure>
              <div class="works__text">
                <p class="works__name">作品名</p>
                <p class="works__date">
                  <time datetime="2021-12-16">2021.12.16</time>
                </p>
              </div>
            </nuxt-link>
          </li>
          <li class="works__item">
            <nuxt-link to="#!" class="works__inner">
              <figure class="works__image">
                <img src="https://placehold.jp/370x229.png" alt="" />
              </figure>
              <div class="works__text">
                <p class="works__name">作品名</p>
                <p class="works__date">
                  <time datetime="2021-12-16">2021.12.16</time>
                </p>
              </div>
            </nuxt-link>
          </li>
        </ol>
        <p class="button-area">
          <nuxt-link to="/works" class="buttonPrimary">view more</nuxt-link>
        </p>
      </div>
    </section>
  </div>
</template>

<style lang="scss" scoped>
.mainVisual {
  img {
    width: 100%;
  }
}

.profile {
  &__upper {
    display: flex;
    flex-direction: column-reverse;
    margin-bottom: 0.5em;

    @include mq() {
      flex-direction: row-reverse;
      justify-content: space-between;
      margin-bottom: 2em;
    }
  }

  &__text {
    @include mq() {
    }
  }

  &__name {
    font-size: fz(24);
    font-weight: bold;
    margin-bottom: 0.5em;

    @include mq() {
      font-size: fz(28);
      margin-bottom: 0.857em;
    }

    [lang='en'] {
      font-size: fz(18);

      &::before {
        content: '/';
        margin: 0 0.5em;
      }
    }
  }

  &__item {
    margin-bottom: 0.5em;

    @include mq() {
      margin-bottom: 1em;
    }
  }

  &__title {
    font-size: fz(18);
    font-weight: bold;
    margin-bottom: 0.222em;

    @include mq() {
      margin-bottom: 0.444em;
    }
  }

  &__image {
    width: 100%;
    margin-bottom: 1.75em;

    @include mq() {
      width: 40%;
      margin: 0 2em 0 0;
    }

    img {
      width: 100%;
    }
  }

  &__message {
    white-space: pre-wrap;
  }
}

.works {
  list-style: none;
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;

  &__item {
    width: 100%;

    @include mq() {
      width: calc((100% - 2.5em) / 2);
    }
  }

  &__item + &__item {
    margin-top: 1.5em;

    @include mq() {
      margin: 0;
    }
  }

  &__inner {
    display: block;
  }

  &__image {
    margin-bottom: 0.5em;

    img {
      width: 100%;
    }
  }

  &__name {
    font-weight: bold;
  }

  &__date {
    font-size: fz(14);
  }
}
</style>


Works一覧ページ


/pages/works/index.vueを作成して下記のように入力しましょう。

<template>
  <div class="child">
    <div class="childMainVisual">
      <div class="container container--lg">
        <h1 class="childMainVisual__title">Works</h1>
        <p>成果物をご紹介します。</p>
      </div>
    </div>

    <div class="container">
      <ol class="row works">
        <li class="works__item">
          <nuxt-link to="/works/111" class="works__inner">
            <figure class="works__image">
              <img src="https://placehold.jp/370x229.png" alt="" />
            </figure>
            <div class="works__text">
              <p class="works__name">作品名</p>
              <p class="works__date">
                <time datetime="2021-12-16">2021.12.16</time>
              </p>
            </div>
          </nuxt-link>
        </li>
        <li class="works__item">
          <nuxt-link to="#!" class="works__inner">
            <figure class="works__image">
              <img src="https://placehold.jp/370x229.png" alt="" />
            </figure>
            <div class="works__text">
              <p class="works__name">作品名</p>
              <p class="works__date">
                <time datetime="2021-12-16">2021.12.16</time>
              </p>
            </div>
          </nuxt-link>
        </li>
      </ol>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.childMainVisual {
  text-align: center;
  padding: 5em 0;
  background-color: $base-color-secondary;
  margin-bottom: 2.5em;

  @include mq() {
    margin-bottom: 5em;
  }

  &__title {
    font-family: $font-ubuntu;
    font-size: fz(40);
    margin-bottom: 0.25em;
  }
}

.works {
  list-style: none;

  &__item {
    width: 100%;

    @include mq() {
      width: calc((100% - 2.5em) / 2);
    }
  }

  &__item + &__item {
    margin-top: 1.5em;

    @include mq() {
      margin: 0;
    }
  }

  &__inner {
    display: block;
  }

  &__image {
    margin-bottom: 0.5em;

    img {
      width: 100%;
    }
  }

  &__name {
    font-weight: bold;
  }

  &__date {
    font-size: fz(14);
  }
}
</style>


Works詳細ページ


/pages/works/_id/index.vueを作成しましょう。
Nuxtではファイル名かディレクトリ名にアンダースコアを使うことによって動的なルーティングができるようになります。
ファイル名かディレクトリ名かの違いは、ページを必須にするかどうかです。詳しくは公式ドキュメントをご確認ください。

改めて、/pages/works/_id/index.vueに下記を入力します。

<template>
  <div class="child">
    <div class="worksMainVisual">
      <div class="container container--lg worksMainVisual__inner">
        <div class="worksMainVisual__contents">
          <h1 class="worksMainVisual__title">作品名</h1>
          <dl class="worksMainVisual__item">
            <dt class="worksMainVisual__itemName">リリース日</dt>
            <dd><time datetime="2021-12-16">2021.12.16</time></dd>
          </dl>
          <dl class="worksMainVisual__item">
            <dt class="worksMainVisual__itemName">制作期間</dt>
            <dd>3ヶ月</dd>
          </dl>
          <p>
            複素数体であれば、任意のCM-タイプの A
            は、実際、数体である定義体(英語版)(field of
            definition)を持っている。自己準同型環の可能なタイプは、対合(ロサチの対合(英語版)(Rosati
            involution)をもつ環として既に分類されていて、
          </p>
        </div>
        <figure class="worksMainVisual__thumbnail">
          <img src="https://placehold.jp/540x334.png" alt="" />
        </figure>
      </div>
    </div>

    <div class="container">
      <dl class="worksItem">
        <dt class="worksItem__title">URL</dt>
        <dd class="worksItem__contents">
          <a href="" target="_blank">https://test.com/</a>
        </dd>
      </dl>
      <dl class="worksItem">
        <dt class="worksItem__title">ポジション</dt>
        <dd class="worksItem__contents">テキスト</dd>
      </dl>
      <dl class="worksItem">
        <dt class="worksItem__title">担当</dt>
        <dd class="worksItem__contents">テキスト</dd>
      </dl>
      <dl class="worksItem">
        <dt class="worksItem__title">技術</dt>
        <dd class="worksItem__contents">テキスト</dd>
      </dl>
      <dl class="worksItem">
        <dt class="worksItem__title">ツール</dt>
        <dd class="worksItem__contents">テキスト</dd>
      </dl>
      <dl class="worksItem">
        <dt class="worksItem__title">アピールポイント</dt>
        <dd class="worksItem__contents">
          吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。
          しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。
          掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。この時妙なものだと思った感じが今でも残っている。第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。その後猫にもだいぶ逢ったがこんな片輪には一度も出会わした事がない。のみならず顔の真中があまりに突起している。そうしてその穴の中から時々ぷうぷうと煙を
        </dd>
      </dl>
    </div>

    <p class="button-area">
      <nuxt-link to="/works" class="buttonPrimary buttonPrimary--leftArrow"
        >back</nuxt-link
      >
    </p>
  </div>
</template>

<style lang="scss" scoped>
.worksMainVisual {
  padding: 5em 0;
  background-color: $base-color-secondary;
  margin-bottom: 2.5em;

  @include mq() {
    margin-bottom: 5em;
  }

  &__inner {
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    flex-direction: column-reverse;

    @include mq() {
      flex-direction: row;
    }
  }

  &__contents {
    @include mq() {
      width: 39.58%;
    }
  }

  &__thumbnail {
    width: 100%;
    margin-bottom: 1.75em;

    @include mq() {
      width: 56.25%;
    }

    img {
      width: 100%;
    }
  }

  &__title {
    font-size: fz(32);
    margin-bottom: 0.125em;
  }

  &__item {
    margin-bottom: 0.5em;
  }

  &__itemName {
    font-size: fz(18);
    font-weight: bold;
    margin-bottom: 0.222em;
  }
}

.worksItem {
  &__title {
    font-size: fz(24);
    font-weight: bold;
    margin-bottom: 0.333em;
  }

  &__contents {
    white-space: pre-wrap;

    span + span {
      &::before {
        content: '/';
        display: inline-block;
        margin: 0 0.5em;
      }
    }
  }

  + .worksItem {
    margin-top: 1.5em;
  }
}
</style>


これで必要なページが揃いました。

APIの取得と表示


各ページができたので、microCMSからAPIを取得して入力したデータを反映させていきましょう。
microCMSではNuxtと簡単に連携できるモジュール、nuxt-microcms-moduleをリリースしているのでこれを使っていきます。

使い方や詳細については下記の記事をご覧ください。
microCMSのNuxt.js用モジュールを公開しました

環境変数を作成


microCMSのAPIを取得するためには、サービスIDとAPIキーが必要になります。
この値は環境変数で管理しましょう。

プロジェクトルートに.envというファイルを作成して、ご自身の環境に合わせて値を入力してください。

GET_API_KEY=dd786b48c45344139fc3cd04474c2d64e0fb
SERVICE_DOMAIN=myportfoliosite


GET_API_KEYはサイドバーの上部歯車マークを押して、サービス設定 > APIキー > default と進んだAPIキーの値をコピーして使います。
SERVICE_DOMAINにはmicroCMS管理画面のサブドメイン(xxxxx.microcms.ioのxxxxx部分)を入力します。
こちらは最初にmicroCMSで作成したサービスIDと同じです。

nuxt-microcms-moduleのインストールと設定


続いてnuxt-microcms-moduleをインストールしましょう。

$ yarn add nuxt-microcms-module


nuxt.config.jsで設定します。入力したら念のためサーバーを再起動しておきましょう。

export default {
  modules: [
    'nuxt-microcms-module', // 追加
  ],

  microcms: {
    options: {
      serviceDomain: process.env.SERVICE_DOMAIN,
      apiKey: process.env.GET_API_KEY,
    },
    mode: process.env.NODE_ENV === 'production' ? 'server' : 'all',
  },
};


APIを取得


管理画面の投稿一覧のAPIプレビューから実際に取得されるデータを事前に確認することができます。



この情報を見ながら設定していくので開発に役立ててみてください。

では、早速APIを取得して表示させていきます。まずはworksから。
/pages/index.vueに下記を追記していきましょう。

<script>
export default {
  async asyncData({ $microcms }) {
    const works = await $microcms.get({
      endpoint: 'works',
      queries: { limit: 2 },
    })
    return {
      works,
    }
  },
}
</script>


外部からデータを取得する場合はasyncData()を使用します。
TOPページ用に表示件数をqueriesで上限2件に絞る設定にしていますのでお好きなように増減させてみてください。

これでtemplate内で{{ works }}で取得したデータを出力することができるようになりました。

同じように/pages/works/index.vueにも下記を追記します。

<script>
export default {
  async asyncData({ $microcms }) {
    const works = await $microcms.get({
      endpoint: 'works',
    })
    return {
      works,
    }
  },
}
</script>


works一覧ページではTOPページと異なり、queries: { limit: 2 },を削除しました。
limitのデフォルトは10件なので、10件以上表示させたい場合にはlimitの指定をしてください。

works一覧を表示


取得したデータをテンプレートに流し込みましょう。TOPページとworksの<ol></ol>の部分を下記のように直します。

        <ol class="row works">
          <li v-for="work in works.contents" :key="work.id" class="works__item">
            <nuxt-link :to="`/works/${work.id}/`" class="works__inner">
              <figure class="works__image">
                <img
                  :width="work.thumbnail.width"
                  :height="work.thumbnail.height"
                  :src="work.thumbnail.url"
                  :alt="work.title"
                />
              </figure>
              <div class="works__text">
                <p class="works__name">{{ work.title }}</p>
                <p class="works__date">
                  <time :datetime="work.release">{{ work.release }}</time>
                </p>
              </div>
            </nuxt-link>
          </li>
        </ol>


これでworksの一覧ができました。

works詳細ページの表示


/pages/works/_id/index.vueに下記のscriptを追加します。

<script>
export default {
  async asyncData({ $microcms, params }) {
    const work = await $microcms.get({
      endpoint: `works/${params.id}`,
    })
    return {
      work,
    }
  },
}
</script>


一覧ページのscriptと異なりendpointにコンテンツIDを指定して、ひとつのコンテンツを取得しています。

詳細ページで取得できるデータは、管理画面のコンテンツ編集ページ上部にある、APIプレビューから確認できます。


取得したデータに沿って/pages/works/_id/index.vueのtemplate内を下記のように修正しましょう。

<template>
  <div class="child">
    <div class="worksMainVisual">
      <div class="container container--lg worksMainVisual__inner">
        <div class="worksMainVisual__contents">
          <h1 class="worksMainVisual__title">{{ work.title }}</h1>
          <dl v-if="work.release" class="worksMainVisual__item">
            <dt class="worksMainVisual__itemName">リリース日</dt>
            <dd>
              <time :datetime="work.release">{{ work.release }}</time>
            </dd>
          </dl>
          <dl v-if="work.term" class="worksMainVisual__item">
            <dt class="worksMainVisual__itemName">制作期間</dt>
            <dd>{{ work.term }}</dd>
          </dl>
          <p v-if="work.overview">{{ work.overview }}</p>
        </div>
        <figure class="worksMainVisual__thumbnail">
          <img
            :width="work.thumbnail.width"
            :height="work.thumbnail.height"
            :src="work.thumbnail.url"
            :alt="work.title"
          />
        </figure>
      </div>
    </div>

    <div class="container">
      <dl class="worksItem">
        <dt class="worksItem__title">URL</dt>
        <dd class="worksItem__contents">
          <a :href="work.url" target="_blank">{{ work.url }}</a>
        </dd>
      </dl>
      <dl v-if="work.position" class="worksItem">
        <dt class="worksItem__title">ポジション</dt>
        <dd class="worksItem__contents">{{ work.position }}</dd>
      </dl>
      <dl v-if="work.responsibility" class="worksItem">
        <dt class="worksItem__title">担当</dt>
        <dd class="worksItem__contents">
          <span
            v-for="(res, resIndex) in work.responsibility"
            :key="resIndex"
            v-text="res"
          />
        </dd>
      </dl>
      <dl class="worksItem">
        <dt class="worksItem__title">技術</dt>
        <dd class="worksItem__contents">
          <span
            v-for="(skill, skillIndex) in work.skill"
            :key="skillIndex"
            v-text="skill"
          />
        </dd>
      </dl>
      <dl v-if="work.tools" class="worksItem">
        <dt class="worksItem__title">ツール</dt>
        <dd class="worksItem__contents">
          <span
            v-for="(tool, toolIndex) in work.tools"
            :key="toolIndex"
            v-text="tool"
          />
        </dd>
      </dl>
      <dl class="worksItem">
        <dt class="worksItem__title">アピールポイント</dt>
        <dd class="worksItem__contents">{{ work.points }}</dd>
      </dl>
    </div>

    <p class="button-area">
      <nuxt-link to="/works" class="buttonPrimary buttonPrimary--leftArrow"
        >back</nuxt-link
      >
    </p>
  </div>
</template>


各項目に実際のデータを流し込みました。
microCMSのAPIスキーマの設定で複数選択可のセレクトフィールドにしていた『担当』『技術』『ツール』は配列で取得されるので、spanタグをv-forでレンダリングさせています。
また、必須項目ではないものにはv-ifを使用してレンダリングさせないようにしています。

日付をフォーマットする


microCMSでデータを取得すると日付が下記のようになりますので、こちらを見やすいようにフォーマットしましょう。
2019-11-09T15:00:00.000Z

フォーマットにdate-fnsというモジュールを使用します。

$ yarn add --dev @nuxtjs/date-fns


nuxt.config.jsで設定をします。

  buildModules: [
    '@nuxtjs/date-fns',  // 追加
  ],


これで$dateFns.format(日付, 形式)のように、使用することができます。

フォーマットしたい日付を下記のように修正しましょう。
microCMSで取得される日付は文字列形式なのでDateオブジェクトに変換して使用しています。

<time
    :datetime="work.release"
    v-text="$dateFns.format(new Date(work.release), 'yyyy.MM.dd')"
  />


これでworksページの完成です!

プロフィールやサイト設定もAPIから取得してみよう


余力があればプロフィールやメインビジュアルの画像もAPIから取得してしまいましょう。

microCMSでAPIを作成


microCMSの管理画面で新しくAPIを作成しましょう。
設定という名前でAPIを作成しました。
プロフィールは投稿するものではないので、オブジェクト形式で作成します。




スキーマ設定ではメインビジュアル以外を必須としました。


完了を押したら、実際に入力して保存しましょう。

APIの取得と表示


/pages/index.vueでsettingsを追加で取得します。scriptを下記のように修正しましょう。

export default {
  async asyncData({ $microcms }) {
    const settings = await $microcms.get({
      endpoint: 'settings',
    })

    const works = await $microcms.get({
      endpoint: 'works',
      queries: { limit: 2 },
    })
    return {
      settings,
      works,
    }
  },
}


template内のメインビジュアルを下記のように修正します。

    <div class="mainVisual">
      <picture>
        <source
          :width="settings.mainVisualSp.width"
          :height="settings.mainVisualSp.height"
          :srcset="settings.mainVisualSp.url"
          media="(max-width: 767px)"
        />
        <img
          :width="settings.mainVisualPc.width"
          :height="settings.mainVisualPc.height"
          :src="settings.mainVisualPc.url"
          alt=""
        />
      </picture>
    </div>


プロフィール部分も修正しましょう。

        <div class="profile">
          <div class="profile__upper">
            <div class="profile__text">
              <p class="profile__name">
                <span>{{ settings.name }}</span>
                <span lang="en">{{ settings.nameEnglish }}</span>
              </p>
              <dl class="profile__item">
                <dt class="profile__title">技術スタック</dt>
                <dd>{{ settings.skills }}</dd>
              </dl>
              <dl class="profile__item">
                <dt class="profile__title">趣味</dt>
                <dd>{{ settings.hobby }}</dd>
              </dl>
            </div>
            <figure class="profile__image">
              <img
                :width="settings.profileImage.width"
                :height="settings.profileImage.height"
                :src="settings.profileImage.url"
                :alt="settings.name"
              />
            </figure>
          </div>
          <p class="profile__message">{{ settings.message }}</p>
        </div>


管理画面で入力したデータが表示されれば完成です!

Netlifyにデプロイ


ここまでできればあとはNetlifyでデプロイしましょう。
Netlifyのアカウント登録済かつGitHubでリポジトリを管理していることを前提に進めます。

NetlifyのTOPより、Add new site > Import an existing project を選択します。


そのままGitHubと連携し、該当のリポジトリを選択します。
デプロイ設定ではBuild commandに注意してください。SSGの場合は下記に修正します。


環境変数も設定します。
Show advanced > New variable とボタンを押して設定項目を表示させます。
.envに登録したGET_API_KEYとSERVICE_DOMAINを設定しましょう。


最後にDeploy siteを押して完了です。
しばらくすればデプロイが完了してサイトが閲覧できるようになります。

Webhookの設定


Netlifyと連携してmicroCMSから更新があった場合にビルドするようにします。
まずはNetlifyから作業します。

Build & deploy > Build hooks で追加していきます。
Build hook nameにmicroCMS(なんでも良いです)として、Saveボタンを押しましょう。
ここで表示されるURLを後ほど使用するのでコピーしておいてください。


続いてmicroCMSでの操作になります。
worksのAPIを選択して右上に表示されるAPI設定から、Webhook > 追加 と進みます。
Netlifyを選択して先ほどのURLを入力します。その他の設定はお好きに変更してください。


設定のAPIも同様に設定しましょう。
同じNetlifyのURLで大丈夫です。

これでmicroCMSで更新があった際にビルドが走るようになりました。
SSGにする場合には忘れないようにしましょう。

最後に


microCMSで簡単なポートフォリオを作ってみました。
完成したコードはGitHubに上げていますので、よく分からなかった箇所があれば見てみてください。
microcms / microcms-portfolio-template - GitHub

今回はコピペで作れるように設計しています(拙いコードで恐縮です)が、『どのようにデータが取得できて、どのように出力しているのか』に着目してコードを読み解いてもらえたら、更なる学びにつながることと思いますのでぜひやってみてください。

Nuxtが初心者の方であればコンポーネントの作り方や粒度、Sassの変数設定なども意識して改変してみるのも良いと思います!
実際に運用するのであれば、headの設定などもしてみてくださいね。

今後はこのポートフォリオにお問い合わせ機能などの拡張も予定していますので、よろしければまたチャレンジしてみてもらえると嬉しいです。

大変お疲れ様でした!

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

しょうみゆ
Web広告運用、インフラ・フロントエンドエンジニアの経験があり、現在はフリーランスとしてWeb制作やフロントエンド開発をメインに活動中。microCMSとNuxtのJAMstack構成での開発が得意。