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 の書き換え(通常リンク + islands hook)
// 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 のテンプレートとして構築支援もできます。
次にやりたいこと(例:履歴操作・アニメーション・パンくずリスト生成など)があれば教えてください!

公開日
更新日