はしがき
先日、自身のポートフォリオサイトをNuxt 3とNewtを使って制作したので、備忘録も兼ね、記事を書いていく。
経緯
このサイトを制作しようと考えた経緯として、僕が既に就活性という立場にある。以前からポートフォリオサイトのようなものは存在していた。今回のこのサイトは以下のサイトを作り直した形になる。
http://i2024040.html.xdomain.jp
元々上記のサイトのサーバは大学の授業で作ったサーバだった。授業内での課題をあげていくサーバだったのだが、課題数が多くなり、いちいちURLを叩くのが面倒臭くなったため、トップページからそれぞれ過大に飛べるようにしようと考えたのが始まりだった。それが、どうせなら全ての課題や作品をまとめてみれるようなポートフォリオ的なサイトにしようと考え、出来上がった。
問題点
このサイトの問題点として、更新するときにコードをいちいち書き換えなければならないことにある。このサイトはCMSなども一切使わずに制作した。これはサーバの問題でもあったが、無料のレンタルサーバを利用していたため、HTMLしか使えなかった。また単にCMSについて知識がなかったことも理由に挙げられる。
更新する際にはフォルダを作って、ファイルをコピーして、中の文字を書き換える……という作業を行っていた。これが非常に面倒臭い。また少し改変したいと思っても全てのHTMLを書き換えなければならないので面倒臭かった。
また表示スピードも遅かった。
これの大半の原因は画像ファイルであるものの、それを除いても表示されるまでに結構時間がかかり、人に見せる時に少し待たせてしまう時間が発生してしまった。これも改善したかった。
サイト刷新
今年の一月頃に僕が所属している研究室のサイトを作った。その時は、研究室のメンバー全員がニュースなどを更新できるようにしたかったので、CMSを流石に使おうと考えた。WordPressを使ってサイトを構築して作ったが、知識も何もなかったので一から調べて何とか完成させた。
自身のサイト構築の時も利用できるぐらいまで知識がついたものの、WordPressを採用しようとは考えていなかった。様々なサイトを見ていく中でヘッドレスCMSを使っているサイトがちらほらある。特に多いのは「Vue.js」やら「Nuxt.js」やら「React」やら「Next.js」やらという文字である。どうせだったらこれらの技術を使ってみたいと考えた。
ノーコード制作ツールやAIの台頭も頭にあった。
下調べ
まず最初に始めたことは、Vue.jsやらReactやらについて調べることだった。個人的にちょっと気になった、Vue.jsを調べてこれでサイトが作れないかと思い、調べてみた。
Vue.js
多くの記事やサイトがVue.jsについて説明しているが、ドキュメントには以下のように書かれている。
Vue (発音は /vjuː/、view と同様) は、ユーザーインターフェースの構築のための JavaScript フレームワークです。標準的な HTML、CSS、JavaScript を土台とする、コンポーネントベースの宣言的なプログラミングモデルを提供します。シンプルなものから複雑なものまで、ユーザーインターフェースの開発を効率的に支えるフレームワークです。
(引用:https://ja.vuejs.org/guide/introduction.html )
こういう時ドキュメントを見ようとよく言われているが、初心者がいきなりドキュメントを見てもよくわからない。しかしVue.jsは親切にチュートリアルがあり、少し触ってみることができた。
https://ja.vuejs.org/tutorial/#step-1
Vue.jsのチュートリアル
Vue.jsは、HTMLとCSSとJavaScriptを単一ファイルコンポーネント(SFC)で管理するためわかりやすい印象を持った。HTMLを使い慣れている身としては大変気に入り、Vue.jsで本格的にサイトを作っていこうと考えた。
しかしながら、Vue.jsでどのようにすればサイトが作れるのか全くわからなかった。一からサイトを作っている記事などを参照したいと考えていたが、ほとんど出てこなかった。
Vue.jsを調べながらNuxt.jsという言葉も見つけた。これについても調べてみようと考えた。
Nuxt.js
公式ドキュメントには以下のように書かれている。
Nuxt is a free and open-source framework with an intuitive and extendable way to create type-safe, performant and production-grade full-stack web applications and websites with Vue.js.
Nuxtは、Vue.jsでタイプセーフ、パフォーマンス、プロダクショングレードのフルスタックWebアプリケーションやWebサイトを作成するための直感的で拡張可能な方法を備えた、フリーでオープンソースのフレームワークです。
( https://nuxt.com/docs :DeepL翻訳 )
よくわかっていないが、Nuxt.js(Nuxtと云うことが多いらしい。以下Nuxt)Vue.jsを使いやすくしたもの、という印象。このNuxtで色々調べるとサイトを作った、的なブログがたくさん見つかったのでこれで作ることにする。
とはいえ、何もわかっていない状態で、サイトを作った、というブログはあるものの、サイトをこのようにして作る、というものは中々見つからなかった。親切に動画を解説している人やサイトを何とか見つけたので作り始めることにした。
Nuxt 3のインストール
Nuxt 3は任意のディレクトリに以下のコマンドでインストールする。
npx nuxi init (プロジェクト名)
完了するとプロジェクト名のフォルダが作成される。cd
でフォルダに移動する。
cd (プロジェクト名)
インストールする。
npm install
これでNuxt 3のインストールが完了する。以下のコマンドを実行して、ローカルサーバを立ち上げる。
npm run dev
http://localhost:3000
にアクセスできるのでブラウザを開くと以下のような画面が出る。
この画面が出ればインストールは完了している。僕の環境ではダークモードを使用しているため黒いが、ライトモードだと白くなる。
ここから先は下記のURLの記事を参考にやっていった。
https://reffect.co.jp/vue/nuxt3
大変参考になりました。ありがとうございます。
最初に作成されるapp.vue
は当初以下のようになっている。
<!--- app.vue --->
<template>
<div>
<NuxtWelcome />
</div>
</template>
この<NuxtWelcome/>
がNuxtの初期画面を映し出していたコンポーネントである。このままではこの画面から一生離れられないので、削除して<NuxtPage/>
と書き換える。
<!--- app.vue --->
<template>
<div>
<NuxtPage />
</div>
</template>
このようにすると初期画面から真っ白の画面になり、何も表示されないようになる。プロジェクトファイル直下に、pages
というフォルダを作り、その下にindex.vue
というファイルを作成する。このindex.vue
に色々書いていくと画面が表示されるようになる。
例えばindex.vue
に以下のように書くと画面上に「Main Page」と表示される。
<!--- pages/index.vue --->
<template>
<div>
<h1>Main Page</h1>
</div>
</template>
このpages
フォルダにファイルを追加しページを作っていくことになる。ページの名前やファイル名がそのままリンクになる。
またページ全体にヘッダーなど共通のレイアウトを適用させることもできる。layouts
フォルダを作り、default.vue
というファイルを作る。ナビゲーションなど追加すると全てのページに同じナビゲーションを適用させられる。
<!--- layouts/default.vue --->
<template>
<div>
<nav>
<ul>
<li>メニュー1</li>
<li>メニュー2</li>
<li>メニュー3</li>
</ul>
</nav>
<slot />
</div>
</template>
<slot/>
は必須。
このままではおそらく表示されないため、app.vue
を以下のように書き換える。
<!--- app.vue --->
<template>
+ <NuxtLayout>
- <div>
<NuxtPage />
- </div>
+ </NuxtLayout>
</template>
このようにすることで表示させることができる。ここまで設定すればapp.vue
はあまりいじらない。後はindex.vue
やpages
フォルダなどにファイルを作っていき完成させる。
CMSの検討
ある程度完成させた後、CMSを検討することにした。ヘッドレスCMSを使いたかったのでそれを中心に色々調べてみることにした。
個人的によく見かける、「Contentful」と「microCMS」を軽く調べてみる。
https://qiita.com/cheez921/items/81cba28e4b815709f863
このサイトに様々なCMSの比較がされてあった。最初は国産であるmicroCMSを使おうかと思っていたが、Contentfulを使うことにした。無料プランでの充実さを考えた時にContentfulがいいのではないかと思った。
しかし英語で、さらに使い方もよくわからなかったので、断念した。
そこでどこで見つけたのかは憶えていないが、「Newt」というCMSを見つけたので、それを使うことにした。使っている人は少ないが、チュートリアルが豊富で丁寧に説明されていたのでまずはやってみることにした。
Newtの導入
チュートリアルの通り進めていく。
https://www.newt.so/docs/tutorials/get-contents-in-nuxt
手順通りにNewtのアカウントを作る。Googleで登録できたのでそちらで作る。チュートリアルはテンプレートを使っていたが、自分は「タイプを選択して追加」を選び、自分でモデルを構成した。
大体はテンプレートと同じだが、作品の写真を載せたかったので「画像」と作品をつくった日もわかるようにしたかったので「日付」を入れた。
Newtの管理画面でAPIの発行と、 「スペースUID」と「App UID」と「モデルUID」を確認した後プロジェクトファイルに戻る。
プロジェクトファイル直下に、.env
ファイルを作成し、スペースUIDとAPIを以下のように設定する。
# .env
NUXT_NEWT_SPACE_UID=(確認したスペースUID)
NUXT_NEWT_CDN_API_TOKEN=(発行したAPI)
次にnuxt.config.tsを以下のように追加する。
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
newt: {
spaceUid: '',
cdnApiToken: ''
}
},
newt-client-jsのインストール
NewtのAPIを参照する時に便利なSDKであるnewt-client-jsをインストールする。
npm install newt-client-js
そしてplugins
フォルダにnewt.server.ts
というファイルを作成し、以下のように記述する。
//plugins/newt.server.ts
import { createClient } from 'newt-client-js'
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig()
const newtClient = createClient({
spaceUid: config.newt.spaceUid,
token: config.newt.cdnApiToken,
apiType: 'cdn'
})
return {
provide: {
newtClient
}
}
})
投稿を取得
Newtのコンテンツを取得するために以下の設定を行う。
types
というフォルダを作成し、その中にarticle.ts
を作成し以下のように記述する。これらtitle
などはモデルを設定した時の「フィールドUID」と呼ばれるものを参照している。今回自作したのでtitle
やbody
の他にheadImage
とdate
をフィールドIDとして設定していたので、以下のように設定する。
// types/article.ts
export interface Article {
_id: string
title: string
slug: string
body: string
headImage: HTMLImageElement
date: Date
}
投稿一覧を作成する
投稿一覧を作成したい場合は、一覧にさせたいファイルの<script>
に以下のように記述する。
<!--- pages/index.vue --->
<script lang="ts" setup>
import type { Article } from '~/types/article'
const { data } = await useAsyncData('articles', async () => {
const { $newtClient } = useNuxtApp()
return await $newtClient.getContents<Article>({
appUid: 'blog',
modelUid: 'article',
query: {
select: ['_id', 'title', 'slug', 'body']
}
})
})
const articles = data.value?.items
</script>
中段頃にある、「appUid」と「modelUid」はそれぞれ自分のidを設定する。
query
の場所では自分のモデルのフィールドIDに合わせて設定することができる。たとえば画像を取得したい場合は以下のように書く。
query: {
select: ['_id', 'title', 'slug', 'body', 'headImage', 'date']
}
投稿一覧を複数作成する
投稿一覧を複数作成する場合は、NewtからAppを新しく設定する。
左のメニューから「Appを追加」を押しAppを作成する。「タイプを選択して追加」を選んだ場合、Appタイプを選び、App名とApp UIDを決める。モデルを作り、モデルUIDを控える。このApp UIDとモデルUIDを上記のappUid
とmodelUid
の場所に書く。
例えば、App UIDが「portfolio」、モデルUIDが「work」である場合は、以下のように書く。
<!--- pages/index.vue --->
<script lang="ts" setup>
import type { Article } from '~/types/article'
const { data } = await useAsyncData('articles', async () => {
const { $newtClient } = useNuxtApp()
return await $newtClient.getContents<Article>({
appUid: 'portfolio',
modelUid: 'work',
query: {
select: ['_id', 'title', 'slug', 'body']
}
})
})
const articles = data.value?.items
</script>
これをページごとに設定することで複数の投稿を取得することができる。
僕はindex.vue
はappUid: 'portfolio'
、blog.vue
にappUid: 'blog'
とした。こうすることで、index.vue
にアクセスした際は作品一覧が読み込まれ、blog.vue
にアクセスするとブログの記事が読み込まれる、という算段。
当初はportfolio
というAppにportfolio
、blog
、info
の3つのモデルを作り、modelUid
をそれぞれ書き換える方法をとっていたのだが、うまくいかなかったので断念し、Appを作ることにした。Newtは無料でもAppを無制限で作ることができるので、ありがたい(六月から新料金になり、ここから新しく作る場合は無料プランでApp数は十個までとなる。詳しくはNewtの公式サイトを参照)。
後は以下のようにtemplate
を編集することで一覧を表示することができる。
<!--- pages/index.vue --->
<template>
<div>
<ul>
<li v-for="article in articles" :key="article._id">
<NuxtLink :to="`/work/${article.slug}`">
{{ article.title }}
</NuxtLink>
</li>
</ul>
</div>
</template>
<li>
などのタグは環境によって変化する。{{article.title}}
などでタイトルを取得することができる。
詳細ページの作成
投稿の詳細は以下のように取得する。大体は同じ感じで、appUid
とmodelUid
を変更する。
詳細ページは[param]のようにページ名を括弧で囲むことで動的なルーティングを行うことができるそう(チュートリアル参考)。ポートフォリオはwork/(作品名)
のようなパスで表示させたいので、pages
フォルダにwork
というフォルダを作成し、その中に[slug].vue
を作成する。
<!--- pages/[slug].vue --->
<template>
<main class="main">
<h2>{{ article?.title }}</h2>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="article?.body" />
</main>
</template>
<script lang="ts" setup>
import type { Article } from '~/types/article'
const route = useRoute()
const { slug } = route.params
const { data } = await useAsyncData(`article-${slug}`, async () => {
const { $newtClient } = useNuxtApp()
return await $newtClient.getFirstContent<Article>({
appUid: 'blog',
modelUid: 'article',
query: {
slug,
select: ['_id', 'title', 'slug', 'body']
}
})
})
const article = data.value
</script>
このようにして、Newtのコンテンツを取得することができる。
画像を読み込ませる
Newtの画像を取得するのに少し手こずった。
画像の取得には以下のように記述すると取得することができた。
<!--- pages/index.vue --->
<img :src="article?.headImage.src">
Cloudflare Pagesでホスティングする
ホスティングをするサービスも少し調べてみる。このときはドメインを取得することは考えておらずドメインをそのまま使いたいという思いがあり、あまり主張の激しくないドメインがいいなあという我侭があった。よってnetify.app
のようになるNetifyは選択肢から外れ、Vercelも無料プランではそこまで充実していないと判断し、Cloudflareを利用することにした。
GitHubのリポジトリとCloudflare Pagesを連携させてホスティングするチュートリアルがNewtによって提供されていたのでこれを参考にする。
VS codeとGitHubを連携
事件
私はVS codeでコーディングを行っていたので、VS codeとGitHubを連携させようと考えた。しかし、以前のプロジェクトで使ったリポジトリとの連携がまだ有効で、何故したのかは思い出せないが、そのままプッシュを行ってしまった。そのリポジトリはすでに存在せず、存在しないリポジトリにプッシュしてしまった。そして、何故か、ファイルがほとんど消滅した。
ファイルが無くなっていたのは、フォルダの中にあったフォルダだった。つまり、プロジェクトファイル直下のフォルダとファイルは残っていた。図に表すと以下の通り。
プロジェクト名
└ pages
└ index.vue(生存)
└ work(消滅)
└ [slug].vue(消滅)
結構ショックだったので記載します……。リポジトリは確認したほうがいいです……。
プッシュできない問題
ファイルを何とかがんばって修復してリポジトリとの連携も終わり、後はプッシュだけとなったところでまたつまずいた。プッシュができない問題が発生した。学校でも開発を行っていたが、学校のWi-FiだとGitHubがプッシュできないのでそれが原因だと思った。が、Wi-Fiを変えてもうまくいかなkった。調べまくると、以下の記事がヒットした。
https://ios-docs.dev/20210813support-for-password/
どうやらローカルからアクセスする際に、GitHubのパスワードではなく個人アクセストークンが必要だそう。サイトの通りにアクセストークンを発行し、入れるとうまくいった。
これでGitHub リポジトリは準備完了。
GitHub リポジトリとCloudflare Pagesの連携
次はNewtのチュートリアルの通り、Cloudflareを使ってホスティングする。
https://www.newt.so/docs/tutorials/connect-to-cloudflare-pages
Newtのコンテンツが表示されない問題
手順通りに進めてサイトにアクセスしても、Newtのコンテンツが表示されなかった。この問題は全く解決しなかった。
チュートリアルの中に、node.jsのバージョンを12以上にしないといけないとのこともあり、この問題かと思い環境変数の数字をいろいろ変えましたが駄目だった。
NewtのAPIを参照しているのがnuxt.config.ts
のnewt
のところだが、そこがうまくいっていないのかと考えた。調べると以下の記事を見つけ、うまくいきそうだったので試すことにした。
https://zenn.dev/reinsp5/articles/89a4767554d7da
記事の通りに書き換えてもうまくいかなかった。正直何が間違っていて何が正しいのかわからない状態に陥った。
とりあえずローカルから直接Cloudflare Pagesにあげてみようと考えた。以下のドキュメントを参考に必要なものをインストールする。
https://developers.cloudflare.com/pages/platform/direct-upload/
wrangler
というものをインストールする。
npm install wrangler
インストールが完了したら以下のコマンドでCloudflareにログインする。
wrangler login
ブラウザが起動するのでログインする。ログインが完了したら次のコマンドを実行する。
wrangler pages project create
このコマンドを実行するとプロジェクトの名前は何ですかと訊かれるので、好きなプロジェクト名をつける。
プロジェクトを作れたら次のコマンドで公開する。
wrangler pages publish (ディレクトリ)
ディレクトリには公開したいディレクトリを指定する。例えばNuxtは大抵dist
に生成されるので以下の通りにする。
wrangler pages publish dist
無事に公開できた。(プロジェクト名).pages.dev
というアドレスにアクセスするとページが表示され、Newtのコンテンツも表示されていた。どうもGitHubと連携して公開する方法がうまくいかないようだった。
Cloudflare Pagesではなく、Workersならうまくいくのではないかとも思ったが、そもそもこのWorkersが何者かよくわかっていなかったし、NewtのWebhookになかったので断念した。Newtのコンテンツが更新されたときにデプロいもしてくれるというNewtのWebhookを使いたかったので、何とかできる方法を模索した。
ちなみにNetlifyも試したがうまくいかなかった。
GitHub Actionsを使ってCloudflare Pagesにデプロイする
いろいろ調べた結果、こちらの方法を試してみることにする。
https://zenn.dev/nwtgck/articles/1fdee0e84e5808
GitHub ActionsでプッシュされたときにNuxt 3をビルドして、Cloudflare Pagesにデプロイする、という方法。
もろもろ設定を終わらせて、試しましたが、これもうまく表示されていなかった。
もう八方塞がりでしたがこちらの記事を見つけた。
https://blog.gini.co.jp/cicd-firebase-and-newt/
これはfirebaseだが、その中に環境変数の設定で、NewtのAPIを設定して、以下のようにしている。
env:
TOKEN: ${{ secrets.TOKEN }}
あまり理解はしていないが、ヘッドレスCMSはビルド時にAPIリクエストをしているらしい……? つまり今まで僕の環境ではビルド時にうまくリクエストができていなかった。この方法ではうまくいくのではないか……? と試したところ……うまく表示されるようになった。
後は以下のチュートリアルの通り、Newtのコンテンツが更新されたときにこのGitHub Actionsを実行するように設定する。
https://www.newt.so/docs/tutorials/trigger-github-actions-with-webhooks
うまくいくことを確認した。
これで無事にサイトを公開することができた。
終わりに
本当はゆっくりと勉強してちゃんと理解してから行ったほうがよかったが、就活のためにも使いたいと考えたため、急いですることになった。
デザイン自体は2月後半頃から初め、悩みながら3月頃には完成していた。HTMLで先に作っていたほうが後々楽なのではないかと考え、作っていたが、それが中々完成できず、泣く泣くデザインを作り直すことにした。それが4月の12日頃だった。
デザインを新しくし、HTMLで作った後、Nuxtをインストールして始めたのが、4月20日頃。
正直ここまで完成するとは思っていなかったが、何とか完成してよかった。全く理解していない状態で詳しいことはわからないままなので、間違いがあれば教えて欲しいです。また、こうしたほうがいいというアドバイスもお願いいたします。
長い記事ではありますが、ご一読ありがとうございました。
またよろしくお願いいたします。