Himawari Project Logo

Himawari Project

ブログ作るなら最初からSSGをしろ!

ブログ作るなら最初からSSGをしろ!

8/3/2025

#React

あなたは、ITエンジニアですか?

あなたは、ITエンジニアですか?

きっと、ここでYESと答えるエンジニアは多いことでしょう。

私は、これまでいくつかの業務系アプリケーションを作ってきました。

しかし、1人で実運用に耐えられるアプリケーションを作成していなかったことと、ITエンジニアなら自身のテックブログくらい自分で作るべきだと考え、この度Himawari Projectという個人事業を始めるとともに、Webサイト兼テックブログを書くことを始めました。

拙い部分もあるかと思いますが、どうかお付き合いください。

Webサイトの種類

一般的に見るサイトには、ソース(HTML,CSS,JavaScript)を配信する方式には、以下の3つがあるようです。

• SPA (Single Page Application) に向いているアプリケーション: 

   ◦ 管理画面系 

   ◦ SEOやSNSでのシェアがそれほど重要ではない、またはURLがあまり関係ないアプリケーション。これは、SPAではHTMLの内容がどのページでも基本的に同じになり、JavaScriptでレンダリングし直すため、OGP(Open Graph Protocol)が正しく表示されない可能性があるためです。また、GoogleのクローラーはJavaScriptを解釈すると言われているものの、SEOが主要な流入経路となるサイトではリスクが残るとされています。

• SSR (Server Side Rendering) に向いているアプリケーション:  

  ◦ 大規模なサービス   

 ◦ リアルタイム性が求められるアプリケーション    

◦ 万能型であり、従来のWebアプリケーションのあり方に近いとされています。SEOやOGPの問題がなく、ユーザーのアクセスごとに適切な情報をサーバサイドでレンダリングして返すためです。ただし、サーバの管理(デプロイ、バージョンアップ、セキュリティアップデート、メンテナンスなど)が必要となり、コストと手間がかかる点がデメリットです。

• SSG (Static Site Generation) に向いているアプリケーション:    

◦ ブログ    

◦ ドキュメントサイト    

◦ 企業サイト    

◦ これらは、コンテンツが頻繁に更新されない、静的な情報中心のサイトに適しています。SSGは事前にビルド時にHTMLを生成するため、非常に高速な表示が可能であり、静的ファイルをホスティングするだけで済むためサーバの維持管理コストが低いというメリットがあります。これらの分類は、開発するWebサイトやアプリケーションの目的、コンテンツの性質、更新頻度、SEOの重要性、運用・保守のコストなどを考慮して最適な構成を選択する上で役立ちます。

私が作成するアプリケーションは、業務系アプリケーションばかりだったので、社内に閉じたものでした。しかもシェアするシチュエーションは全くありません。当然の如く今回のブログサイト制作もSPAで制作を始めてしまいました。

あ、やばいと思った読者の方。大丈夫です。まだ、間に合います(笑)

SPAだと何がまずいのか

SNSのシェアと、SEOに圧倒的に不利です。

SPAだと、サーバーからindex.htmlがクライアント側に渡されるわけですが、同じページ内で遷移しているように見えながらも、実際には完全なページ遷移は行われません。  

ユーザーがクリックしても、最初からHTTPリクエストを投げに行くのではなく、JavaScriptがURLを書き換え、そのURLに基づいて必要な情報だけを再度取得します。

そのため、GoogleやXのクローラーは、JavaScriptを解釈してインデックスを行うわけですが解釈はあくまで努力目標でしかなく、実際にはTOPページしかインデックスされていないというケースが大半です。

また、SPAではOpenGraph(XにURL乗せると自動でサムネとか生成してくれるやつ)に対応するのがプログラム的にもクローラー的にも難しいです。

  • プログラム的に難しい

    • コンポーネント分割したアプリケーションでは、動的にmetaタグを付け替える必要がある。それをやってくれるライブラリも用意されているが、OpenGraphやクローラーに認識されるかは運ゲー。(TypeScriptもとい、JavaScript依存)
  • クローラー的に難しい

    • プログラム的に難しいとほぼ同じ原理ですが、JavaScriptを多少なりとも認識していますがそれがどこまでかはわからないため、ときに他のサイトよりもSSGよりもSPAが遅いため、クローラーがタイムアウトする可能性だってあります。

上記の理由から、SEOやシェア機能に非常に悪影響を及ぼしてしまうのです。

じゃあ、どうやってSSGするの?

1つの失敗

読者の中には、なんでこんなことわざわざ記事にしているの?と考える人もいることでしょう。そう。私は、最初の選択を間違えたのです(笑)

業務で、Viteを使ったので何も考えずに本プロジェクトのフレームワークにもViteを採用してしまいました。Nextであれば、設定を変えるだけでSSGが可能です(これは、数多のテックブログが解説しているので省く)がViteはできません。

落とし穴

vite-plugin-ssrというプラグインがあるらしい。しかし、バージョンアップかなんかでVikeという名前に変更された。

やっかいなことにVikeになってからの記事はそんなに多くない。(公式的には、名前が変わっただけで、機能は何も変わっていないらしい)

Vikeを使ってみよう

まずは、使った結果から。結論ファースト。

ASIS

.
├── .github
│   └── workflows
│       └── deploy.yml
├── public
│   ├── ...
├── src
│   ├── components
│   │   ├── Footer.tsx
│   │   ├── Header.tsx
│   │   ├── NewsSection.tsx
│   │   ├── ScrollToTop.ts
│   │   └── VideoCard.tsx
│   ├── App.css
│   ├── App.tsx
│   ├── Home.tsx
│   ├── index.css
│   ├── License.tsx
│   ├── main.tsx
│   ├── PrivacyPolicy.tsx
│   ├── ProjectPolicy.tsx
│   ├── SoftwareDevelopment.tsx
│   ├── VideoProduction.tsx
│   ├── vite-env.d.ts
│   └── YouTube.tsx
├── .gitignore
├── eslint.config.js
├── index.html
├── LICENSE
├── package-lock.json
├── package.json
├── postcss.config.js
├── README.md
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

TOBE

.
├── .github
│   └── workflows
│       └── deploy.yml
├── public
│   └── ....
├── scripts
│   └── generate-posts.mjs
├── src
│   ├── components
│   │   ├── Footer.tsx
│   │   ├── HatenaIcon.tsx
│   │   ├── Header.tsx
│   │   ├── MarkdownComponents.tsx
│   │   ├── NewsSection.tsx
│   │   └── VideoCard.tsx
│   ├── content
│   │   ├── blog
│   │   │   ├── article
│   │   │       ├── ...
│   ├── pages
│   │   ├── blog
│   │   │   ├── @slug
│   │   │   │   ├── +config.ts
│   │   │   │   ├── +data.ts
│   │   │   │   ├── +description.ts
│   │   │   │   ├── +onBeforePrerenderStart.ts
│   │   │   │   ├── +Page.tsx
│   │   │   │   └── +title.ts
│   │   │   ├── +config.ts
│   │   │   ├── +data.ts
│   │   │   └── +Page.tsx
│   │   ├── index
│   │   │   ├── +config.ts
│   │   │   └── +Page.tsx
│   │   ├── license
│   │   │   ├── +data.ts
│   │   │   └── +Page.tsx
│   │   ├── privacy
│   │   │   ├── +data.ts
│   │   │   └── +Page.tsx
│   │   ├── project
│   │   │   ├── +data.ts
│   │   │   └── +Page.tsx
│   │   ├── software
│   │   │   └── +Page.tsx
│   │   ├── video
│   │   │   └── +Page.tsx
│   │   ├── youtube
│   │   │   └── +Page.tsx
│   │   ├── +config.ts
│   │   ├── +Head.tsx
│   │   └── Layout.tsx
│   ├── styles
│   │   └── index.css
│   ├── types
│   │   ├── pageContext.ts
│   │   ├── pageContextPost.ts
│   │   ├── pageContextPosts.ts
│   │   └── Post.ts
├── .gitignore
├── eslint.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── postcss.config.js
├── README.md
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.tsbuildinfo
└── vite.config.ts

見ただけで、悪寒がしますね。実務だと大混乱と大遅延を巻き起こします。

詳しく何をやってるかは、公式のホームページ(https://vike.dev/)で翻訳をかけて見てもらうとしてここでは、何をやったかをざっくり解説します。

前提として、Vite + Reactの構成からの導入を行うものとします。

作業

インストール

npm i vike vike-react

vite.config.tsに以下の設定を加えます。

defineConfig.plugins.vike()

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import vike from 'vike/plugin';
import tailwindcss from 'tailwindcss'
import autoprefixer from 'autoprefixer'
import path from 'path';

export default defineConfig({
  plugins: [
    react(),
    vike(),
  ],
  css: {
    postcss: {
      plugins: [
        tailwindcss,
        autoprefixer
      ]
    }
  },
  ssr: {
    noExternal: ['tailwindcss']
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
});

css、ssr、resolveの設定はなくてももしかたら動くかも。AIに聞きまくっていたところなので不要な設定も入り込んでいるかもしれません。

フォルダ構成

TOBEのフォルダ構成を見ていただくと分かる通り、src/pages配下にwebページにアクセスしたときのパスとその配下に、+XXX.ts(x)というファイルがあります。

src/pages/indexだけは、webページの「/」にリダイレクトされます。いわゆる、ホームページをここに作っておくと良さそうです。

コンポーネント

ここでは、+XXX.ts(x)を解説しておこうと思います。結構複雑なので、若干正確性に欠部分がありますがご容赦ください。

先頭に+を付けて、フレームワーク側で定義されている名前をつけると、その機能として認識してくれます。

+Page.tsx

クライアントサイドで表示する画面を定義できます。tsxなので、基本的にReact.FCを返して終わりです。

ふつーのReactとあんまり変わりません。

サンプルコードです。

import Header from "../../components/Header";
import Footer from "../../components/Footer";
import ReactMarkdown from "react-markdown";
import { usePageContext } from "vike-react/usePageContext";
import { PageContext } from "../../types/pageContext";
import { markdownComponents } from "../../components/MarkdownComponents";

const License: React.FC = () => {
  const pageContext = usePageContext() as { data: PageContext };
  const content = pageContext.data?.content || "読み込み中...";
  
  return (
    <>
      <Header />
      <section id="license" className="py-12">
        <div className="container mx-auto px-4 sm:px-6 lg:px-8">
          <ReactMarkdown components={markdownComponents}>{content}</ReactMarkdown>
        </div>
      </section>
      <Footer />
    </>
  );
};

export default License;

その他、+data.tsや+onBeforeRender.tsからのデータを受け取ることができます。

+data.ts と +onBeforeRender.ts

サーバーサイドで実行されるデータ取得ロジックです。

例えば、データベース接続のために、SQLを投げたり(ORMでもOK)APIリクエストを投げたりして、その結果を+Page.tsxに返してあげます。

+data.tsとonBeforeRender.tsの違いは、+Page.tsxへのデータの返し方にあります。

+data.tsでは、pageContext.data.xxxに格納されるのに対して、+onBeforeRender.tsではpageContext.xxxでデータを返してあげることができます。(誰得?)

例で見てみましょう

+data.ts

import fs from "node:fs";
import path from "node:path";
import matter from "gray-matter";

export function data() {
  // プロジェクトのルートディレクトリからの相対パスでMarkdownファイルを読み込みます
  const filePath = path.join(process.cwd(), "src", "content", "license.md");
  const fileRawContent = fs.readFileSync(filePath, "utf-8");

  // gray-matterでファイル内容をパースし、本文(content)とフロントマター(data)を分離します
  const { content } = matter(fileRawContent);
  return {
    content, // pageContext.data.contentで+Page.tsxで受け取れます。
  };
}


+onBeforeRender.ts

// /pages/some-page/+onBeforeRender.js
 
export function onBeforeRender() {
  const someValue1 = /* ... */
  const someValue2 = /* ... */
  // pageContext.prop1 === someValue1
  // pageContext.prop2 === someValue2
  return {
    pageContext: {
      prop1: someValue1, //pageContext.prop1
      prop2: someValue2 //pageContext.prop2で受け取れます
    }
  }
}

pageContext以外も渡せるよ〜ってところが違いなんですかね。(使い方をよくわかっていない)

ブログサイトのSSGするなら、あまり関係ない部分だと思ってもらっていいです。基本的には+data.tsを使いましょうで済む話です。

で、+data.tsをどこで使うかと言ったら、ブログデータやらユーザデータやらがデータベースに入っている場合でしょうか。今回、リポジトリにダイレクトで突っ込んでいるので、そのあたりの技術は使いませんでした。

+config.ts

これが、すごくわかりにくい。

title, description, faviconなどのmetaタグ情報を入れたりできるが、OGPの情報(og:title)は入れられない。

唯一、忘れないでほしいことは、pages/+config.tsにprerender:trueの設定をしておくことだ。

これを入れないと、SSGではなく、SSRとなるらしい。SSRでも問題ない場合はそれでいいのだが今回はさくらのレンタルサーバーを使っているので、必ずtrueにする。

設定の例はこんな感じだ。

// +config.ts
import vikeReact from "vike-react/config";
import { Layout } from "./Layout";
import { description, favicon, title } from "../const/pageConstants";

export default {
  Layout,
  lang: "ja",
  extends: vikeReact,
  prerender: true,
  passToClient: ["content"],
  favicon,
  title,
  description,
};

Layoutは、最上位となるコンポーネントですべての子コンポーネントがこの最上位コンポーネント上にレンダリングされる。煩わしい表現だが、コードで見ると明らかである。

// Layout.tsx
import React from "react";
import type { PageContext } from "vike/types";
import "../styles/index.css";

export function Layout({
  children,
}: {
  children: React.ReactNode;
  pageContext: PageContext;
}) {
  return <React.StrictMode>{children}</React.StrictMode>;
}

総括

Viteにvite-plugin-ssrを入れれば、すぐにSSGができるだろうと見積もっていて3人日もあれば終わるなと思っていたのが、Vikeを導入することになり、大幅にサイトの構成を変えることになった。

実プロジェクトだと、本当に痛い目を見ていただろうが個人の趣味のサイトだから、対費用効果なんて関係ない。

DevinやClineなんかの最新AIに頼っても見たが、全く違う路線に走ってしまう(暴走)傾向があり、そのくせ稼働分はしっかりお金を取ってくるというなんとも言葉にしがたい感覚である。

DevinなどのAIかもしっかり仕様書を書いておければ、ある程度開発効率は上がるのだろうが、個人のサイトで仕様書なんか書きたくない。

コードばっかり書いていた人間、コードで仕様を語る人間には、到底AIコーディングなんかは無理だろう(私もどちらかというとその傾向がある人間だが・・・)

何が言いたいかって言うと、Devinを入れてすべて解決します。1人月分稼ぎますというのはAIって話題に乗っか解けば儲かるコンサルとAI使ってますというと安心できる経営者のまやかしでしか無い。

と、妄想してみます。

もちろん、複雑な業務ロジックもなく、ちょっとしたアプリを作るにはDevinやClineは有効であろうかとは思うが。それ以上のレベルを求めると、レビューと修正の無限ループが発生するので(その間のACUやTokenも消費する)結局は、人間が落とし所をつけてコーディングするしか無い。

ジュニアレベルのエンジニアは淘汰されて、代替される。(順序、分岐、反復が理解できたくらいの人)

かといって、誰でもジュニア時代はあるわけで。

どうするかといったら、自分のWebサイトを作るのが一番手っ取り早い。

  • 言語はどうする?(Java?Ruby?JavaScript)。
  • フレームワークはどうする?(Spring Boot?Ruby on Rails?React?)。
  • どのサーバーで動かす?(Azure?AWS?Netlify?CloudFrare?オンプレミス?)
  • CI/CDはどうする?(GitHub Actions?)
  • データベースは?(RDS?NoSQL?)
  • ORMは?
  • 予算は?

などなど。考えることはたくさんあります。

他にも、構築していく過程でエラーやどうにも解決不可能な問題にぶち当たることでしょう。

仕事では、あっさり解決困難なら回避しましょうとなりますが、ここは、個人の世界なので、無限に時間を投入できます(飽きたり、現状の技術では対処しようがないものに関してはもちろん回避賛成ですが)。納得の行くまで突き詰めると、意外なところで約に立つことがあったりします。

それに、そのプロセスこそが、あなたの技術力を磨く作業にほかならないです。(と勝手に信じてる)

どうか、AIに操られてCMD+VとコンソールにEnterを押すだけの人間にはならないでください。

少なくとも、エンジニアを名乗っているうちだけは。

そうこうしているうちに、次のプロジェクトの話が来たようだ。

なになに。

次のプロジェクトでは、既存の契約システムをモダンなシステムに置き換える。

Markdownで仕様書を書き、Devinでコーディング、人間はレビューに徹すること。

クライアントもAIを使った初のコーディングで、弊社に大変期待をしている。

死んでも成功させること。

線香の香りが漂っていた。