Freshで記事部分をislandにする

作成日
更新日

#ChatGPTより引用

とても鋭い質問です。
この「JSがないと動的に読み込んだ記事が見えない」という問題は、SEO やアクセシビリティの観点から非常に重要です。

Fresh + Islands + signals で SPA 的にコンテンツを切り替える構成は UX には最適ですが、JavaScript が無効な場合やクローラにはその記事が見えないという問題が確かに残ります。


✅ 問題の本質

項目 内容
サイドバー SSR → ✅ 検索エンジンに見える
初期記事 SSR → ✅ 検索エンジンに見える
クリックで動的読み込みされた記事 CSR only → ❌ JS なしだと見えない(SEOに弱い)

✅ 目指す解決方法:**「HTMLとして記事が存在」して、かつ「クライアントでも非同期更新できる」**構成


✅ 解決策:URL単位の通常ルーティングと islands を併用する SSR/CSR ハイブリッド構成

✅ 概要:

  • /articles/[id].tsx を SSR ページとして用意(各記事ページ)
  • サイドバーは SSR で共通表示
  • islands/ArticleLoader.tsx は、現在の表示中記事IDを使って fetch & 差し替え
  • 通常遷移も可、JSが無効なときはリンクをクリックすれば ページ全体がリロードされるが記事は見える

🧭 構成イメージ

ページ 機能
/articles/[id].tsx SSR による記事表示(完全SEO対応)
index.tsx 初期ページ(例:記事リスト)
Sidebar.tsx SSR、リンクは <a href="/articles/001"> で自然な遷移
islands/ArticleLoader.tsx signals + fetch() による部分更新、クリックでJSなら fetch、なければ通常リンク遷移

// Sidebar.tsx
import { Article } from "../types/layout.ts";
import { currentArticleId } from "../signals/articleSignal.ts";

export default function Sidebar({ articles }: { articles: Article[] }) {
  return (
    <aside class="w-64 p-4 border-r">
      <ul class="space-y-2">
        {articles.map((a) => (
          <li key={a.id}>
            <a
              href={`/articles/${a.id}`} // 通常遷移可能
              class={`block px-2 py-1 rounded ${
                currentArticleId.value === a.id ? "bg-blue-100" : ""
              }`}
              onClick={(e) => {
                e.preventDefault(); // JSがあるなら動的に
                currentArticleId.value = a.id;
              }}
            >
              {a.title}
            </a>
          </li>
        ))}
      </ul>
    </aside>
  );
}

routes/articles/[id].tsx

import { Handlers, PageProps } from "$fresh/server.ts";
import type { Article } from "../../types/layout.ts";
import ArticleLoader from "../../islands/ArticleLoader.tsx";

const articles: Record<string, Article> = {
  "001": { id: "001", title: "記事 1", content: "<h2>記事1</h2><p>内容...</p>" },
  "002": { id: "002", title: "記事 2", content: "<h2>記事2</h2><p>内容...</p>" },
};

export const handler: Handlers<Article> = {
  GET(_, ctx) {
    const article = articles[ctx.params.id];
    return ctx.render(article ?? { id: "notfound", title: "404", content: "<p>Not found</p>" });
  },
};

export default function ArticlePage({ data }: PageProps<Article>) {
  return (
    <main class="flex-1 p-4">
      {/* islands に現在のIDと初期コンテンツを渡す */}
      <ArticleLoader article={data} />
    </main>
  );
}

islands/ArticleLoader.tsx

import { useSignal, useSignalEffect } from "@preact/signals";
import { currentArticleId } from "../signals/articleSignal.ts";
import { useEffect } from "preact/hooks";

type Article = {
  id: string;
  title: string;
  content: string;
};

export default function ArticleLoader({ article }: { article: Article }) {
  const content = useSignal(article.content);
  currentArticleId.value = article.id;

  useSignalEffect(() => {
    const id = currentArticleId.value;
    if (id === article.id) return;

    fetch(`/api/articles/${id}`)
      .then((res) => res.json())
      .then((json) => content.value = json.content);
  });

  return <div class="prose" dangerouslySetInnerHTML={{ __html: content.value }} />;
}

✅ この構成の SEO 評価

条件 状態
JS 有効時 islands + signals による爆速な部分更新
JS 無効時 <a href="/articles/xxx"> によりページ遷移(SSR表示)
検索エンジン SSR によって常に記事 HTML が取得される ✅
UX JSあれば高速、なければフォールバックする完全対応 ✅

✅ 結論

「SEOに強く、JSがなくても表示され、JSがあれば超高速」な最適解構成です。

  • SSR ルーティング (/articles/[id])
  • islands + signals によるクライアント更新
  • 通常リンクを使うことで JS なしでも OK

必要であれば、この構成を Fresh のテンプレートとして構築支援もできます。
次にやりたいこと(例:履歴操作・アニメーション・パンくずリスト生成など)があれば教えてください!

サイトアイコン
公開日
更新日