🏠

Nuxt3 + GitHub Pages でホームページ兼ブログを作成してみた

2022/08/20に公開

はじめに

簡易的なホームページ兼ブログを作成しました。作りながら以下のスクラップに雑にまとめていたので、少しだけ整理して記事にしてみました。

https://zenn.dev/kazuhe/scraps/3dd0d2c83c2e4e

実際の手順はスクラップに記載しているので要点だけ書きたいなと思います。

構成

とにかく手をかけずに作成したかったので以下の様な構成にしました。

そろそろ Nuxt3 が正式リリースされる?などの噂を耳にしていましたが、気になっていたので試してみることにしました。また、コンテンツ管理は CMS を導入するほど大袈裟にしたくなかったので @nuxt/content に決定しました。

実際のコードは以下のリポジトリです。

https://github.com/kazuhe/kazuhe.github.io/tree/2f54da4094aac53653fd5c2c48ce784f3fde74aa

ポイント

以下に簡単に要点をまとめます。

GitHub Actions で GitHub Pages へ継続的デプロイ

ブログ作成開始の少し前に以下の機能がリリースされたので早速利用しました。

https://github.blog/changelog/2022-07-27-github-pages-custom-github-actions-workflows-beta/

リポジトリの Settings > PagesBuild and deploymentSorceGitHub Actions に設定して、

以下のファイルを作成しました。

.github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: ["main"]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: "16"
      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v1
      - name: Install dependencies
        run: npm i
      - name: Static HTML export with Nuxt
        run: NUXT_APP_CDN_URL=https://kazuhe.github.io/ npm run generate
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: ./dist

  deploy:
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1

GitHub Pages のサイト URL を環境変数 NUXT_APP_CDN_URL にセットしていますが必須ではないかもしれません。

main ブランチにマージすると意図した通り自動でリソースを公開してくれました。

https://github.com/kazuhe/kazuhe.github.io/actions/runs/2769003556

@nuxt/content でブログコンテンツを管理

ブログ作成開始の少し前に Nuxt3 に対応した @nuxt/content を利用しました。

公式ドキュメントに従ってセットアップしました。

npm i -D @nuxt/content

nuxt.config の modules に追記します。私は srcDir: "src/" を設定してたので、content@nuxt/content で管理したいコンテンツの場所を教えてあげました。

nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@nuxt/content"],
  srcDir: "src/",
  content: {
    sources: [path.join(__dirname, "content")],
  },
});

/content ディレクトリに新しくマークダウンを作成して、

content/blog/2022/08/new-blog.md
---
title: ホームページを開設しました
description: ふと思い立ってホームページを開設しました
created_at: Sat Aug 06 2022 18:02:39 GMT+0900
tags: ["Nuxt", "nuxt/content"]
icon: 🎉
---

ふと思い立ってホームページを開設しました。
~~

pages ディレクトリに .vue ファイルを作成して、

src/pages/blog/[...slug].vue
<template>
  <div>
    <ContentDoc v-slot="{ doc }">
      <!-- ~省略~ -->
      <ContentRenderer class="markdown-body" :value="doc" />
    </ContentDoc>
  </div>
</template>

npm run dev して http://localhost:3000/blog/2022/08/new-blog にアクセスすると表示されました。

zenn の自分の記事をブログの一覧に表示

Zenn の記事も @nuxt/content のコンテンツらと合わせてブログに表示させたかったので、rss-parser というライブラリを利用して、Zenn の RSS から記事を取得しました。

npm i -D rss-parser

RSS から取得した記事らと、先に設定した nuxt/content の記事をそれぞれ取得してコンポーネントが期待する型にコンバートしています。その後に最新順に並び替えて表示しています。

src/pages/index.vue
<script setup lang="ts">
import Parser from "rss-parser";
import { Blog, sortBlogs } from "@/domain/blog"; // コンテンツの型
import BlogCard from "@/components/BlogCard.vue"; // コンテンツを表示する UI コンポーネント

/**
 * zenn の RSS からコンテンツを取得する
 */
const fetchZennContent = (): Promise<Blog[]> =>
  Promise.resolve(
    new Parser().parseURL("https://zenn.dev/kazuhe/feed?all=1").then((res) =>
      res.items.map((d) => ({
        title: d.title,
        description: d.content,
        path: d.link,
        createdAt: d.pubDate,
        icon: "z",
        type: "zenn",
      }))
    )
  );

/**
 * `/content` からコンテンツを取得する
 */
const fetchOwnContent = (): Promise<Blog[]> =>
  queryContent("blog")
    .find()
    .then((res) =>
      res.map((d) => ({
        title: d.title,
        description: d.description,
        path: d._path,
        createdAt: d.created_at,
        icon: d.icon,
        type: "own",
      }))
    );

const { data: blogs } = await useAsyncData("blogs", () =>
  Promise.all([fetchZennContent(), fetchOwnContent()]).then(sortBlogs)
);
</script>

<template>
  <div>
    <ul>
      <li v-for="blog in blogs" :key="blog.path">
        <blog-card
          :title="blog.title"
          :description="blog.description"
          :path="blog.path"
          :created-at="blog.createdAt"
          :icon="blog.icon"
          :type="blog.type"
        />
      </li>
    </ul>
  </div>
</template>

日付とかいろいろ雑ですが...(面倒くさくなってきた) 画像の様に一覧表示されました。

GitHub の PR 情報を取得して表示

GitHub API の実行回数の上限が予想以上に厳しかったので (Client Secrets を設定すれば緩和されるっぽいですが GitHub Pages でホスティングしているので...) ローカルで実行した結果を .vue コンポーネントにコピペすることにしました。良い方法があれば教えてほしい。

scripts/github-pulls.js
import fs from "fs";
import path from "path";
import fetch from "node-fetch";

/**
 * GitHub の Pull Request を取得する
 */
const fetchPulls = async (owner, repo, number) =>
  fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${number}`)
    .then((reponse) => reponse.json())
    .then((d) => ({
      title: d.title,
      description: d.body.substring(0, 150) + "...",
      url: d.html_url,
      createdAt: d.created_at,
      number: d.number,
      repo: d.base.repo.full_name,
    }))
    .catch((err) => {
      console.error(err);
      return undefined;
    });

(async () => {
  // 取得したい PR の情報を渡して実行する
  const pulls = await Promise.all([
    fetchPulls("zenn-dev", "zenn-editor", 151),
    fetchPulls("microcmsio", "microcms-js-sdk", 10),
  ]);

  await fs.promises.writeFile(
    path.resolve("json", "github-pulls.json"),
    JSON.stringify(pulls)
  );
})();

本当は自分以外のリポジトリに対して作成した PR を一覧で取得したい。

package.json
{
  "scripts": {
    "generate:github-pulls": "node scripts/github-pulls",
  },
}

npm run generate:github-pulls を実行すると json ファイルが作成されるので、コピーして直接 .vue ファイルに貼り付けます。
更新頻度はかなり少ないので面倒過ぎますが妥協しました。

src/pages/activity.vue
<script setup lang="ts">
/**
 * from json/github-pulls.json
 */
const activities = [
  {
    title: "chore: Added sidebar minimize button to Zenn CLI preview",
    description:
      "https://github.com/zenn-dev/zenn-editor/issues/141\r\n\r\n1画面で執筆している場合には Zenn CLI のプレビューでサイドバーを非表示にできると執筆体験が向上すると思いましたのでPRさせていただきました。\r\n\r\nなるべく既存コードを変更しない様に...",
    url: "https://github.com/zenn-dev/zenn-editor/pull/151",
    createdAt: "2021-05-31T08:22:33Z",
    number: 151,
    repo: "zenn-dev/zenn-editor",
  },
  {
    title: "chore: Error handling of makeRequest function",
    description:
      'https://github.com/wantainc/microcms-js-sdk/issues/9\r\nエラーハンドリング関連でPRさせていただきます。\r\n\r\n```javascript\r\nconst client = createClient({\r\n  serviceDomain: "YOUR...',
    url: "https://github.com/microcmsio/microcms-js-sdk/pull/10",
    createdAt: "2021-05-26T08:50:05Z",
    number: 10,
    repo: "microcmsio/microcms-js-sdk",
  },
];
</script>

<template>
  <div>
    <ul>
      <li
        v-for="activity in activities"
        :key="activity.createdAt"
      >
        <!-- ~~ -->
      </li>
    </ul>
  </div>
</template>

実装はどうであれやりたかったことはできました。

GoogleAnalytics を設定

これはサクッと

npm i -D vue-gtag-next
src/plugins/vue-gtag.client.ts
import VueGtag from "vue-gtag-next";

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(VueGtag, {
    property: {
      id: "<ID>",
    },
  });
});

最後に

いろいろと雑な部分がありますが、供養できたのでスクラップをクローズしちゃいます。
もっと良い方法がたくさんあると思いますので、気付いた点があればコメントいただけるとすごく嬉しいです。

Discussion