Nuxt3でWebサイト作成してみた

Nuxt使うの初めてです。かなり奮闘したので忘備録としてここにUPしておきます。
NodeとNuxtは下記バージョンをインストール済み、npmコマンドを使用。
・Node.js Ver22.11.0
・Nuxt.js Ver3.14

## Nuxt3の構造
梱包されてた内容は以下だった気がします。

任意のディレクトリ名/
├─ README.md
├─ app.vue
├─ nuxt
├─ node_modules
├─ nuxt.config.ts
├─ package-lock.json
├─ package.json
├─ public
├─ server
├─ tsconfig.json
└─ error.vue

ここに必要なディレクトリを作成しました。
追加
├─ assets
├─ components
├─ layouts
└─ pages

## app.vue
何もなければここがTOPページとなるようです。
下層ページや複数デザインを造りたかったので以下のタグを入れました。
これでpagesが表示できるようになります。
```vue:app.vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
```

## publicディレクトリ
publicにfaviconやSNS用のアイコン、あと外部読み込み用のjsフォルダ作っておきます。

public
├─ favicon.ico
├─ apple-touch-icon.png
├─ og_image.jpg
├─ js
└─ robots.txt

robots.txtは初めから入っていました。

## tsconfig.json
ここにjsフォルダのパスを登録、tscのエラー回避だそうです。
```yaml:tsconfig.json
{
  "extends": "./.nuxt/tsconfig.json",
+  "exclude": [
+    "./public/js/*.js"
+ ],
}
```
## assetsディレクトリ
CSSと画像フォルダ作って格納しておきます。

## nuxt.config.ts
ここは全ページ共通haedを設定しました。
Titleやdescription、css、jsなど個別ページで使用したいものはlayoutsやpagesで別途追記します。
```typescript:nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2024-04-03',
  devtools: { enabled: true },
  ssr:true,
  //assets内の有効化したcssファイルは、全ページで有効化
  css: [ '~/assets/css/sanitize.css',
         '~/assets/css/style.css'
  ],
  app: {
    //共通head
    head: {
      htmlAttrs: {
        lang: "ja",
        prefix: "og: http://ogp.me/ns#",
      },
        charset: 'utf-8',
        title: "Nuxt Test",
        meta: [
            // <meta name="description" content="My amazing site.">
            {name: 'description', content: 'nuxtのテスト.'},
            {name: "viewport", content: "width=device-width, initial-scale=1" },
            { property: "og:site_name", content: "サイト名" },
            { property: "og:type", content: "サイトのタイプ" },
            { property: "og:url", content: "サイトURL" },
            { property: "og:title", content: "サイトタイトル" },
            { property: "og:description", content: "サイトの説明" },
            { property: "og:image", content: "サイトURL" },
            {
              name: "twitter:card",
              content: "summary_large_image",
            },
            { name: "twitter:site", content: "@Twitter" },
        ],
        link: [
          { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
          { rel: 'stylesheet',
            href: 'https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic&display=swap',
            crossorigin: ''
          },
          { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
        ],
        //jsはtsconfig.jsonに登録 publicに格納
        // 読み込む箇所: 'head' | 'bodyClose' | 'bodyOpen'
        script: [
          { type: 'text/javascript', src: '/js/jquery-3.7.1.min.js', tagPosition: 'bodyClose'},
	  { type: 'text/javascript', src: '/js/main.js', tagPosition: 'bodyClose'},
        ],
    }
},
});
```
compatibilityDateの日付は最初から入っていました。

## components
headerやfooter、navなど共通コンポーネントを格納しておきます。
今回のテストサイトはTOPと下層ページのデザインが違うのでheaderやfooterはTOP用で分けました。
本来は少ない方がメンテナンスが良いので、TOPは合体させた方がいいかもです。
components
├─ PageHeader.vue
├─ PageFooter.vue
├─ Footer.vue
├─ NavBar.vue
├─ TestNav.vue
└─ SpNav.vue

```vue:TestNav.vue
<template>
  <nav class="testNav">
    <ul class="rowNav">
      <li><a href="/">HOME</a></li>
      <li><a href="/gsapcode">GSAP CODE</a></li>
      <li><a href="/row">row index</a></li>
      <li><a href="/row/title1">RootTest1</a></li>
    </ul>
    <div class="pageIntro">
      <p>
        ページリンクのテストです。link
        toで繋ぐと何故かjsが効かないのでhref属性を使いました。<br />componentsでパーツ化したTestNavからメニューを入れています。pagesからのルートパスでファイル名に拡張子は付けず、リンクを貼っています。
      </p>
    </div>
  </nav>
</template>
```
NuxtLink To 使うと何故かjsの一部動きませんでした。なのでaタグ使ってます。
hrefの場合も拡張子は必要ないようです。

## layouts
componentsで作ったパーツを組み合わせてテンプレートを作るイメージです。
何も指定しなかったらdefault.vueが適用されます。
今回TOPページをdefaultに設定しました。
layouts
├─ default.vue
└─ code.vue

```vue:default.vue
<script setup lang=ts>
useHead({
  script: [
    { type: "text/javascript", src: "/js/scroll.js", tagPosition: "bodyClose" },
    { type: "text/javascript", src: "/js/gsap.js", tagPosition: "bodyClose" },
    {
      type: "text/javascript",
      src: "/js/ScrollTrigger.js",
      tagPosition: "bodyClose",
    },
    { type: "text/javascript", src: "/js/effect.js", tagPosition: "bodyClose" },
  ],
});
</script>

<template>
  <div class="wrapper">
    <div class="loader"></div>
    <header class="header panel" id="header">
      <div class="topImg">
        <div class="toolTop">
          <dl class="tool">
            <dt>Illustration Tool</dt>
            <dd>Illustrator</dd>
          </dl>
        </div>
        <section class="container">
          <div class="read animeText">MY-PORTFOLIO</div>
          <h1 class="h1Title animeText">Gallery-Nuxt3.ver</h1>
          <h2 class="once animeText">js・php開発用サイト</h2>

          <div class="leftArea eBox1">
            <div class="detailbBox">
              <p class="mb20">
                マイギャラリーサイトに訪問していただき有り難うございます。
              </p>
              <p class="mb20">
                こちらはPHP、js、フレームワーク等の開発用サイトとして立ち上げました。デザインからコーディングまでフルスクラッチで組み立てています。
              </p>
              <p>
                最近、個人的にVue.jsに興味があり時間を見ては触っています。<br />その延長でNuxt3を使ってみることにしました。<br />コマンドラインを走らせながらの作業は慣れてない上、コンポーネント、レイアウトの構成、データの置き場所など扱い方が独特で手探りでの製作です。
              </p>
            </div>
            <div class="subBox">
              <h3 class="explan">製作環境</h3>
              <div class="column2">
                <ul>
                  <li>Nuxt.js 3.14</li>
                  <li>Node.js 22.11.0</li>
                  <li>GSAP 3.12.5</li>
                  <li class="mb20">jQuery 3.7.1.</li>
                  <li>Netfliy</li>
                </ul>
                <ul>
                  <li>Figma</li>
                  <li>Illustrator</li>
                  <li>Photo shop</li>
                  <li>CLIP PAINT SUTADIO</li>
                  <li>Painter</li>
                  <li>Visual Studio Code</li>
                </ul>
              </div>
            </div>
            <p class="update">更新日 2024年12月15日</p>
          </div>
        </section>
      </div>
      <!-- //.header panel -->
    </header>
    <div class="menuBtn" id="menuBtn">
      <img src="~/assets/image/menu.svg" alt="MENUボタン" />
    </div>

    <NavBar />
    <slot />
    <!-- pagesに書き出す場合slotは必須 -->
    <Footer />
    <SpNav />

    <div class="toTop" id="toTop">
      <img src="~/assets/image/to-top.svg" alt="TOPへ" />
    </div>
  </div>
</template>
```
TOPページしか使わないjsをsetupしました。
画像リンクは"~/assets/image/画像ファイル"。
slotはpagesで書き出す場所に置かないと表示しないそうです。
コンポーネントの各場所も指定。

slotはname属性つけて個別に紐付けできるそうですが、よくわからないので今回はパス。

```vue:code.vue
//下層ページ用のレイアウト
<script setup lang=ts>
useHead({
  script: [
    {
      type: "text/javascript",
      src: "/js/prettify.js",
      tagPosition: "bodyClose",
    },
    { type: "text/javascript", src: "/js/code.js", tagPosition: "bodyClose" },
  ],
});
//cssここに設置
import "@/assets/css/prettify.css";
import "@/assets/css/sunburst.css";
import "@/assets/css/page.css";
</script>

<template>
  <div class="wrapper">
    <div class="loader"></div>
    <PageHeader />
    <slot />
    <TestNav />
    <PageFooter />
    <div class="toTop" id="toTop">
      <img src="~/assets/image/to-top.svg" alt="TOPへ" />
    </div>
  </div>
</template>
```
下層ページのテンプレートです。
CSSはここに組み込みました。

## pages
URLのつくファイルになります。
pages/index.vueがTOPページとして表示され、その他のページは下層になり、階層も作れます。
ページタイトルやdiscriptionは個別設定。
```vue:gsapcode.vue
//下層ページ
<script setup>
useHead({
  title: "Gsapについて | My Gallery Site",
  meta: [
    {
      name: "description",
      content: "gsapのコードです",
    },
  ],
});
//default以外のlayoutを使う
definePageMeta({
  layout: "code", //適用しない場合はfalse
});
</script>

<template>
  <main class="pageMain">
	.
	.
	.
  </main>
</template>
```
useHead()でhead書換え
definePageMeta()でレイアウトを切り替えます。

### 特定のディレクトリだけ子ページのファーストビューを統一したいという時
Pages直下でrow.vueというファイルとrowディレクトリを作る(任意の名前)。名前で連動するようです。
インクルードのイメージでしょうか。

例えばrow.vueというページが親になるとします。
![page-row.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641189/63a600eb-7db7-fee6-1350-17681ce4ef68.jpeg)

row.vueは共通部分だけ記述。
```vue:row.vue
//親ページ
<script setup>
definePageMeta({
  layout: "code",
});
</script>

<template>
  <main class="pageMain">
    <section class="container">
      <h2 class="pageTitle">ROOT TEST</h2>
      <div class="parent">
        <p>親要素のrowファイル<br />ここはrow共通部分</p>
      </div>
      <NuxtPage />
    </section>
  </main>
</template>
```
<b>子ページとなるところに&lt;NuxtPage /&gt;を入れるのがミソ。</b>
rowというディレクトリ作ってindex.vueを作成。
```vue:index.vue
//rowの子ページ
<script setup>
useHead({
  title: "row index | My Gallery Site",
  meta: [
    {
      name: "description",
      content: "row階層のindex",
    },
  ],
});
</script>

<template>
  <div>
    <h3 class="rowTitle">row index</h3>
    <div class="pageIntro">
      <p class="mb20">HOME / row / index</p>
      <p class="center">
        ここはrow階層のindex、<br />子要素にindexファイル作ると親のrowファイルではなく子要素のindexが適用されるようです。リダイレクトみたいな感じ。<br />親のrowファイルは同名ディレクトリ以下の共通パーツに
      </p>
    </div>
  </div>
</template>
```
/rowにアクセスすると/row/に飛びます。
ディレクトリにindex.vueを作っておくとリダイレクト処理しなくても自動で飛んでくれるので、部品だけのページが見られることはなくなります。便利。
### 余談ですが・・・
vueでできているならvueコードも使えるはずだと思いテストしてみました。
title.vueというページを作って試しました。
```vue:title.vue
<script setup>
useHead({
  title: "title1 | My Gallery Site",
  meta: [
    {
      name: "description",
      content: "ルートのテスト",
    },
  ],
});

//vueも使えるように
import { ref } from "vue";
const itemLi = [
  { num: "1", text: "タイトル1" },
  { num: "2", text: "タイトル2" },
  { num: "3", text: "タイトル3" },
];

const items = ref(itemLi);
const name1 = ref("v-forの書出し");
</script>

<template>
  <div>
    <h3 class="rowTitle">title1</h3>
    <div class="pageIntro">
      <p class="mb20">HOME / row / title1</p>
      <p class="center">ここはrow階層のtitle1<br />Git hub更新テスト</p>
    </div>

    <h3 class="rowTitle">{{ name1 }}</h3>
    <p class="mt20 center">
      VueでできているならVueも使えるはずだと思い、v-forを差し込んでみました。<br />連想配列のループ書き出しは便利です。scriptの中にref関数で呼び出せました。
    </p>
    <ul class="vTest">
      <li v-for="item in items" :key="item">
        <p>numの番号は {{ item.num }}、textは {{ item.text }}</p>
      </li>
    </ul>
  </div>
</template>
```
v-forを使う際には、"key 属性"を付与しないと予期せぬバグが発生するかもとのことです。key属性推奨。

### componentsをたくさん作って整理したい時
コンポーネントをたくさん作るのは得策ではないと思いますが、いろんなパーツ作ってディレクトリに分けておきたいなどと考えてしまいました。
今回のテストサイトでは試していませんが、とりあえず出来たので追記します。

nuxt.config.tsにパスを登録。
export default defineNuxtConfig()の中に追加。
```ts:nuxt.config.ts
components: [
    { path: '~/components/child' },
    '~/components'
  ],
```
css:の上に追記しました。
componentsにchildディレクトリ作成して任意のコンポーネントを作ります。

components
└── child
&nbsp;	├── PartsA.vue
&nbsp;	└── PartsB.vue
 
 あとはlayoutsでファイル作成。customレイアウト。
 ```vue:custom.vue
 <template>
    <div class="wrapper">
      <h1 class="h1">カスタムレイアウト</h1>
      <NavBar />
      <slot />
      <PartsA></PartsA>
      <PartsB />
      <FooterBar />
    </div>
</template>

<script setup>
import '@/assets/css/sub.css';
</script>
```
&lt;PartsA&gt;&lt;/PartsA&gt;でも&lt;PartsB /&gt;でも書き出せます。

## errorページ作成
error.vueもデザインに合わせてカスタマイズしました。
 ```vue:error.vue
<script setup>
const error = useError();
import "@/assets/css/page.css";
</script>

<template>
  <div class="wrapper">
    <header class="pageHeader">
      <!-- ここはhtmlコピペ -->
    </header>
    <main class="pageMain">
      <section class="container errorP">
        <h1 class="pageTitle" v-if="error.statusCode === 404">404 ERROR</h1>
        <h1 class="pageTitle" v-else>ERROR</h1>
        <p class="errM">{{ error.message }}</p>

        <div class="linkBtn">
          <a href="/">Home</a>
        </div>
      </section>
    </main>
    <footer class="pageFooter">
      <!-- ここはhtmlコピペ -->
    </footer>
  </div>
</template>

<style scoped>
.errM {
  font-size: 2rem;
  text-align: center;
  padding-bottom: 10px;
}

.errorP {
  margin-bottom: 50px;
}
</style>
```
レイアウトは適用できなかったのでheader、footerはhtmlをコピペしました。
## いざ公開に向けて
Git HubとNetlifyを連携する方法が簡単らしいということで、まずはデータをGit Hub Desktopからpush。
エラー出ました。
なんでもファイル数多すぎて100個以上は一度にUPできないとのこと。
仕方なくコマンドラインにトライ・・・がGitのインストールが必要とのこと。sshで接続したかったので鍵も作成。3回くらい作り直しました。
接続に失敗するたびローカルの.sshにknown_hostsというゴミファイルが増えていく・・

Git初期設定と接続は下記URLを参考にしました。
https://prog-8.com/docs/git-env
https://qiita.com/shizuma/items/2b2f873a0034839e47ce

<br>
心機一転、リモートから失敗したリポジトリを捨てて新規で作り直し。ローカルと連携させてデータをpush。

.gitignoreを作成しておいた方がいいとgoogleのGeminiさんが教えてくれたので一応作成。
ーーーーーーーーーーーーーーーー
♯ Nuxt generated files
.nuxt
dist

♯ node modules
node_modules

♯ Editor config
.vscode

♯ Log files
logs
ーーーーーーーーーーーーーーーー

### 最初のコミット
コマンドラインにてローカルのルートディレクトリで以下のコマンドを実行し、Gitリポジトリを初期化。初回の一回のみ実行。
```
% git init
```
Initialized empty Git repository・・・と出ればOK。

コマンドを順番に実行し、すべてのファイルをステージングしてコミットします。

```
% git add .
% git commit -m "任意のコメント"
% git remote add origin git@github.com:[リポジトリの場所]
```
ここでいうmainはブランチ名、他の名前だったら変える
```
% branch -M main
```
下記で聞かれるパスフレーズはGit Hubのログインパスワードでした。
```
% git push -u origin main
Enter passphrase for key '/Users/自分のアカウント/.ssh/id_rsa':
```
### 2回目からの更新
-Aはすべてのファイル変更を追加。
```
% git add -A
% git commit -m "コメント"

#連携できたかの確認
% git remote -v
#変更箇所が出てくる
% git push origin main
```
Git Hub、更新されてました。

## Netlify連携
Netlifyにアカウント作ります。
https://www.netlify.com/

最初、Git Hubの連携を選ばなかったのでメールでアカウントで作成してしまいましたが、
新規サイトを作成してからGit Hubの連携が選べるので特にどちらでもいいようです。

サイトを作る際、色々質問されますが選択方式でGit Hubとの連携を選んでいけばスムーズに進んでいけました。
Build settingsは<b>「nuxt generate」で（npm run buildだと何故かエラー出てしまいました）。</b>
Publish directoryは「dist」になっていることを確認。
「Deploy site」のボタンを押せば後は待つだけ。「Published」と表示が変わります。
これで無事公開できました。

仕事の合間にプライベートでNuxtサイトを作ってきました。
隙だらけの造りかと思いますが、まずは形になったかなと。

## 追記：GoogleアナリティクスGタグ埋め込みました
nuxt-gtagとかモジュールを使う方法が紹介されてましたが、何故か解析が取ませんでした。
下記のように直に埋め込んだら無事カウントされました。
```ts:nuxt.config.ts
script: [
        {
          src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX',
          async: true, tagPosition: 'head'
        },
        {
          innerHTML: `
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'G-XXXXXXXX');
          `,
          type: 'text/javascript',
          charset: 'utf-8',   
        },
        { type: 'text/javascript', src: '/js/jquery-3.7.1.min.js', tagPosition: 'bodyClose' },
        { type: 'text/javascript', src: '/js/main.js', tagPosition: 'bodyClose' },
      ],
      __dangerouslyDisableSanitizers: ['script'],
```
ただ、__dangerouslyDisableSanitizersはセキュリティリスクを伴うのであまり使うのは良くないそうです。scriptをちゃんと書き出したかった為、ここだけに置くことにしました。
<br>
以下が今回作ったテストサイトです。

<a href="https://okamechan.netlify.app/">![thumb.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641189/6bf8eb2e-0fa3-7764-41fb-2bb0d3f0a2b9.jpeg)</a>https://okamechan.netlify.app/