Nuxt3で作ってみた!
今回できるもの
今回はYouTubeのよく見るトップページを、Nuxt3で実装してみました。 この記事を見れば、 ・Webサイトのページがどんなふうにできるのか ・Htmlから作るのとNuxtでやる時の違い ・10分で1ページできてしまうぐらいのNuxt ✖️ Bulmaの相性の良さ ...etc を紹介できればなと思います!
phase.0 まずは構造理解
最初はまず、なにをやるかをはっきりさせましょう。
よく見ると、というか一般的にサイトは、
・ヘッダー
・サイドメニュー
・メイン
に分かれています。
なので今回この三つの部分ごとにソースも含めて解説します。
※BulmaとPugを使用しています。

phase.1 ヘッダーを作ろう
ヘッダー部分は左右と真ん中の三箇所に分かれていて、 ・左のロゴ ・真ん中に検索 ・右にプロフィールなどのアイコン をそれぞれ配置します。
<template lang="pug">
header#mainHeader
nav.navbar.pb-2.content-row-space-between
.content-row-space-left
.navbar-item.navbar-burger(:class="{'is-active':bugerActive}" @click="bugerClick")
span
span
span
.navbar-item
NuxtLink(:to="'/'")
img.image(src="/logo_yp.png")
.content-row-space-left
.navbar-item.control.has-icons-right
input.input#inputLeftRounded(placeholder="検索")
button.button#searchLightRounded
span.icon
i.fas.fa-lg.fa-solid.fa-magnifying-glass
.navbar-item
button.button.is-rounded
span.icon
i.fas.fa-lg.fa-solid.fa-microphone
.content-row-space-left
.navbar-item
button.button.is-rounded
span.icon
i.fas.fa-lg.fa-solid.fa-video
.navbar-item
button.button.is-rounded
span.icon
i.fas.fa-lg.fa-regular.fa-bell
.navbar-item
button.button.is-primary.is-inverted.is-rounded
img(src="/channelImg.png")
</template>
<script setup lang="ts">
const props = defineProps<{
bugerActive: boolean;
}>();
interface Emits {
(e: "update:bugerActive", value: any): void;
}
const emit = defineEmits<Emits>();
function bugerClick() {
emit("update:bugerActive", !props.bugerActive);
}
</script>
<style lang="scss" scoped>
#mainHeader {
position: fixed;
height: 70px;
width: 100%;
z-index: 99990;
}
#inputLeftRounded {
border-radius: 40px 0 0 40px;
}
#searchLightRounded {
border-radius: 0 40px 40px 0;
width: 64px;
}
</style>
phase.3 サイドメニューを作ろう
サイドメニューはもっとシンプル! 縦に並べるだけ。 チャンネル登録者の部分はv-forを使って表現できます!
<template lang="pug">
.modalBack(v-if="bugerActive")
.modal-background(@click="bugerClick")
.modal-content#modalMainSideber
.content-row-space-left
.navbar-item.navbar-burger.p-2.m-0(:class="{'is-active':!bugerActive}" @click="bugerClick")
span
span
span
.navbar-item
NuxtLink(:to="'/'")
img.image(src="/logo_yp.png")
ul.menu-list.border-bottom-light.m-3
li.pl-3.pr-3
NuxtLink.p-2
.content-row-space-left
span.icon
i.fas.fa-lg.fa-regular.fa-house
p.subtitle.is-size-7.m-0.pl-5 ホーム
li.pl-3.pr-3
NuxtLink.p-2
.content-row-space-left
span.icon
i.fas.fa-lg.fa-regular.fa-circle-play
p.subtitle.is-size-7.m-0.pl-5 ショート
li.pl-3.pr-3
NuxtLink.p-2
.content-row-space-left
span.icon
i.fas.fa-lg.fa-tv
p.subtitle.is-size-7.m-0.pl-5 登録チャンネル
ul.menu-list.border-bottom-light.m-3
li.pl-3.pr-3
NuxtLink.p-2
.content-row-space-left
p.subtitle.is-size-7.m-0 マイページ
span.icon
i.fas.fa-lg.fa-solid.fa-angle-right
li.pl-3.pr-3
NuxtLink.p-2
.content-row-space-left
span.icon
i.fas.fa-lg.fa-regular.fa-address-card
p.subtitle.is-size-7.m-0.pl-4 登録チャンネル
ul.has-text-left.m-3
p.subtitle.is-size-7.m-0.p-3 登録チャンネル
li.pl-3.pr-3(v-for="ch in subscedChannelList" :key="ch.channelID")
NuxtLink
ChannelCard(:ch="ch")
aside.sidebar.is-fullheight.is-hidden-mobile#mainSideber
ul.menu-list
li.pt-5
NuxtLink.p-1(:to="'#'" @click="changeActiveLink(0)")
span.icon
i.fas.fa-lg.fa-house(:class="[activeNum == 0 ? 'fa-solid':'fa-regular']")
p ホーム
li.pt-5
NuxtLink.p-1(:to="'#'" @click="changeActiveLink(1)")
span.icon
i.fas.fa-lg.fa-circle-play(:class="[activeNum == 1 ? 'fa-solid':'fa-regular']")
p ショート
li.pt-5
NuxtLink.p-1(:to="'#'" @click="changeActiveLink(2)")
span.icon
i.fas.fa-lg.fa-tv(:class="[activeNum == 2 ? 'fa-solid':'fa-regular']")
p 登録チャンネル
li.pt-5
NuxtLink.p-1(:to="'#'" @click="changeActiveLink(3)")
span.icon
i.fas.fa-lg.fa-photo-film(:class="[activeNum == 3 ? 'fa-solid':'fa-regular']")
p マイページ
</template>
<script setup lang="ts">
const props = defineProps<{
bugerActive: boolean;
}>();
interface Emits {
(e: "update:bugerActive", value: any): void;
}
const emit = defineEmits<Emits>();
function bugerClick() {
emit("update:bugerActive", !props.bugerActive);
}
const activeNum = ref(0);
function changeActiveLink(n: number) {
activeNum.value = n;
}
const subscedChannelList = [
{
channelID: 1,
thumbnail: "/channelImg.png",
name: "チャンネル名1",
},
{
channelID: 2,
thumbnail: "/channelImg.png",
name: "チャンネル名2チャンネル名2",
},
{
channelID: 3,
thumbnail: "/channelImg.png",
name: "チャンネル名3チャンネル名3チャンネル名3",
},
{
channelID: 4,
thumbnail: "/channelImg.png",
name: "チャンネル名4",
},
{
channelID: 5,
thumbnail: "/channelImg.png",
name: "チャンネル名5チャンネル名5",
},
{
channelID: 6,
thumbnail: "/channelImg.png",
name: "チャンネル名6チャンネル名6",
},
{
channelID: 7,
thumbnail: "/channelImg.png",
name: "チャンネル名7チャンネル名7チャンネル名7",
},
];
</script>
<style lang="scss" scoped>
#openedSideber {
min-width: 120px;
}
.sidebar {
position: fixed;
top: 70px;
order: 1;
}
#mainSideber {
width: 68px;
p {
font-size: 10px;
}
}
#modalMainSideber {
position: absolute;
top: 0px;
left: 0px;
background-color: white;
width: 240px;
min-height: 100%;
}
</style>
phase.4 メイン部分を作ろう
メイン部分はほぼなし。 一番重要なのは「動画のサムネイルカード」をコンポーネントでv-forすること。 若干のスペースを開けつつ、タイトルなどを並べましょう。 再生回数や「●日前」などは別途ソースを見てみてください!
<template lang="pug">
#homeView
.tabs.content-row-space-left.p-3.m-0.janruTab
button.button.is-small.mr-3(v-for="janru in janruList" :key="janru.cd" :class="[janru.cd == nowJanru ? 'is-black':'is-light']") {{ janru.title }}
ul.columns.is-multiline.p-4.pt-6
li.column.is-one-third(v-for="mv in TopMovieList")
MovieCard(:movie="mv")
</template>
<script setup lang="ts">
const janruList = [
{ title: "すべて", cd: 0 },
{ title: "ゲーム", cd: 1 },
{ title: "ライブ", cd: 2 },
{ title: "音楽", cd: 3 },
{ title: "ミックス", cd: 4 },
];
const nowJanru = ref(0);
// AIPから取得ならこんな感じ
// const TopMovieList = ref([]);
// const { data, error } = await useFetch(
// ここurl,
// {
// method: "GET",
// headers: {
// "content-type": "application/json",
// },
// body: {
// パラメーター
// },
// }
// );
// if (!error.value) {
// TopMovieList.value = data.value.~~
// }
const TopMovieList = [
{
movieID: 1,
title:
"タイトル1タイトル1タイトル1タイトル1タイトル1タイトル1タイトル1タイトル1",
movie: "/movies/movie_1.mp4",
thumbnail: "/movies/thumbnail_1.png",
views: 33000,
publishedAt: new Date("2023-11-01 12:15:01"),
channel: {
channelID: 1,
name: "チャンネル名1チャンネル名1チャンネル名1チャンネル名1チャンネル名1",
thumbnail: "/channelImg.png",
},
},
{
movieID: 2,
title:
"タイトル2タイトル2タイトル2タイトル2タイトル2タイトル2タイトル2タイトル2",
movie: "/movies/movie_2.mp4",
thumbnail: "/movies/thumbnail_2.png",
views: 11289019,
publishedAt: new Date("2023-01-01 9:15:01"),
channel: {
channelID: 2,
name: "チャンネル名1チャンネル名1チャンネル名1チャンネル名1チャンネル名1",
thumbnail: "/channelImg.png",
},
},
{
movieID: 3,
title:
"タイトル3タイトル3タイトル3タイトル3タイトル3タイトル3タイトル3タイトル3",
movie: "/movies/movie_1.mp4",
thumbnail: "/movies/thumbnail_3.png",
views: 827365189,
publishedAt: new Date("2023-11-30 11:15:01"),
channel: {
channelID: 3,
name: "チャンネル名1チャンネル名1チャンネル名1チャンネル名1チャンネル名1",
thumbnail: "/channelImg.png",
},
},
{
movieID: 4,
title:
"タイトル4タイトル4タイトル4タイトル4タイトル4タイトル4タイトル4タイトル4",
movie: "/movies/movie_2.mp4",
thumbnail: "/movies/thumbnail_4.png",
views: 9810,
publishedAt: new Date("2000-01-01 10:15:01"),
channel: {
channelID: 4,
name: "チャンネル名1チャンネル名1チャンネル名1チャンネル名1チャンネル名1",
thumbnail: "/channelImg.png",
},
},
{
movieID: 5,
title:
"タイトル5タイトル5タイトル5タイトル5タイトル5タイトル5タイトル5タイトル5",
movie: "/movies/movie_1.mp4",
thumbnail: "/movies/thumbnail_5.png",
views: 33000,
publishedAt: new Date("2022-3-01 12:15:01"),
channel: {
channelID: 5,
name: "チャンネル名1チャンネル名1チャンネル名1チャンネル名1チャンネル名1",
thumbnail: "/channelImg.png",
},
},
{
movieID: 6,
title:
"タイトル6タイトル6タイトル6タイトル6タイトル6タイトル6タイトル6タイトル6",
movie: "/movies/movie_2.mp4",
thumbnail: "/movies/thumbnail_6.png",
views: 11289019,
publishedAt: new Date("2023-11-01 10:15:01"),
channel: {
channelID: 6,
name: "チャンネル名1チャンネル名1チャンネル名1チャンネル名1チャンネル名1",
thumbnail: "/channelImg.png",
},
},
];
</script>
<style lang="scss" scoped>
#homeView {
min-height: 88vw;
}
.janruTab {
position: fixed;
z-index: 10;
width: 100%;
background-color: white;
}
</style>
<template lang="pug">
NuxtLink(:to="`/watch?v=${movie.movieID}`")
.card-image
figure.image.is-16by9
img(:src="movie.thumbnail" alt="Thumbnail")
.media.pt-2
.media-left
NuxtLink(:to="`/channel/${movie.channel.channelID}`")
figure.image.is-32x32
img.is-rounded(:src="movie.channel.thumbnail" alt="Channel image")
.media-content.has-text-left
NuxtLink(:to="`watch?v=${movie.movieID}`")
p.subtitle.is-6.m-0.mb-2 {{ movie.title }}
NuxtLink(:to="`/channel/${movie.channel.channelID}`")
p.subtitle.is-7.has-text-grey.m-0 {{ movie.channel.name }}
p.subtitle.is-7.has-text-grey.m-0 {{ $common.millBillUnit(movie.views) }} 回視聴・{{ $common.dateAgo(movie.publishedAt) }}
</template>
<script setup lang="ts">
const props = defineProps<{
movie: any;
}>();
</script>
phase.5 まとめ
今回はYouTubeのトップページのみ解説しました。 他にも、動画ページやチャンネルページも作ってみる予定ですのでお楽しみに!
全体のソースはこちら! https://github.com/yamu-studio/Nuxt3-YouTube/tree/phase_1
