Next.js で NASA の APOD API を使って最新画像を表示した話
きっかけ
- もともとポートフォリオとしてNASAの宇宙写真が表示されるページをNext.jsで作成
- topは最新の画像が1枚、archiveは過去の画像からランダムに7枚表示
- しかし、再度見たら全然ランダムじゃない!
- それどころか作成段階から最新画像が更新されない!
今回行ったこと
- 取得・表示コードの修正
- APIルートの作成
- cloudflarePagesからVercelに移行
原因
- ホスティング
- API取得の仕組みを理解していなかった
元サイト
環境
$node -v
v22.20.0
$npm -v
10.9.3
$npx next --version
Next.js v15.5.4
$ npx tsc -v
Version 5.9.3
$npm list --depth=0
my-portfolio@0.1.0 /home/hogehoge
├── @emnapi/core@1.5.0 extraneous
├── @emnapi/runtime@1.5.0 extraneous
├── @emnapi/wasi-threads@1.1.0 extraneous
├── @eslint/eslintrc@3.3.1
├── @napi-rs/wasm-runtime@0.2.12 extraneous
├── @tailwindcss/postcss@4.1.14
├── @tybys/wasm-util@0.10.1 extraneous
├── @types/node@20.19.19
├── @types/react-dom@19.2.0
├── @types/react@19.2.1
├── eslint-config-next@15.5.4
├── eslint@9.37.0
├── next@15.5.4
├── react-dom@19.1.0
├── react@19.1.0
├── tailwindcss@4.1.14
└── typescript@5.9.3
以下本題
ここからどんな直し方をしたか記載。(駄文なので人が読みやすいようには書いてないです。)
ちなみにChatGPTに手伝ってもらいながら修正しました。
トラブルシューティング
GPTいわく、元のコードの
const res = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=${API_KEY}&count=${count}`,
{ next: { revalidate: 3600 } } //キャッシュが残ってるのではないか?
);
と、なっているのが原因ではないかと?のこと。
const res = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=${API_KEY}`,
{ cache: "no-store" } //最新の一枚だけ出るようにキャッシュ無し!読み込みなおし!
);
と修正。直らず。
次にAPIポイントを作ることで直るのではないかと提案があり、APIのコンポーネントを作成
// app/api/apod/route.ts
import { NextRequest, NextResponse } from "next/server";
type APODData = {
date: string;
title: string;
url: string;
media_type: "image" | "video";
explanation: string;
};
export async function GET(req: NextRequest) {
const API_KEY = process.env.NASA_API_KEY;
if (!API_KEY) return NextResponse.json({ error: "NASA APIキー未設定" }, { status: 500 });
const count = Number(req.nextUrl.searchParams.get("count") || 1);
const res = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=${API_KEY}&count=${count}`,
{ cache: "no-store" }
);
if (!res.ok) {
const text = await res.text();
return NextResponse.json({ error: text }, { status: res.status });
}
const data: APODData[] = await res.json();
// count=1 の場合は配列ではなく単一オブジェクトにする
if (count === 1) return NextResponse.json(data[0]);
return NextResponse.json(data.filter(d => d && d.url));
}
そしてpage.tsxに
"use client";
を追加し、
const res = await fetch("/api/apod?count=1", { cache: "no-store" });
に修正。変わらず。
ここで、cloudflareとの相性が悪いことを教えてもらえる。
核心 Cloudflare Pages は 静的サイト、あなたの Next.js は サーバーが必要
以下、ChatGPTの解説
● Cloudflare Pages = 静的ホスティング
- HTML / CSS / JS を置くとそのまま配信
- Next.js の SSR(Server Side Rendering)や API Routes は使えない
- “ビルド時にすべて作っておいてね” という静的サイト前提の世界
あなたのプロジェクトは…
● あなたの Next.js = サーバーが必要な構造
- API Routes(/api/apod)を使う
- NASA API に サーバー側で安全にアクセスしたい
- cache: "no-store" など 常に最新データを取得する仕組みが必要
- revalidate や RSC の動的フェッチを利用したい
この時点で両者はギアが噛み合わない。
Cloudflare Pages は Next.js をサポートしている…と言われていますが、サポートしているのは TurboPack 生成の SSR 対応モードか、静的生成モードのみです。
Cloudflare Pages でエラーが出た理由
Cloudflare Pages は Next.js をサポートしている…と言われていますが、サポートしているのは TurboPack 生成の SSR 対応モードか、静的生成モードのみです。
あなたの構成だと以下の問題が発生しました:
① cache: "no-store" が静的エクスポートと衝突
Cloudflare Pages の build モードは 静的出力(Static Export) を強制していました。
ただし Next.js の静的出力は、動的フェッチ(no-store)と同時使用できません。
結果:
「静的だよね?」
「いや動的フェッチしたい」
「無理です」
という衝突 → ビルドエラー。
② output: "export" が API Routes を破壊していた
Cloudflare に合わせようとして
output: "export"
- /api/apod が生成不可
- API Routes は SSR 必須なので静的出力と両立しない
- 結果的に NASA API にアクセスするロジックが壊れる
③ Client Side Fetch に逃がしても“環境変数が使えない”問題
Cloudflare Pages でクライアント側から NASA API を呼ぶと、API KEY を公開するしかない。
これはセキュリティ的にアウト。
対して Vercel が「そのまま動いた」理由
● Vercel = Next.js の本拠地
- Next.js の開発元(Vercel 製)
- API Routes はフル対応
- RSC / サーバーコンポーネントも完全サポート
- cache: "no-store" など SSR 特有の動的フェッチをそのまま許容
- output: "export" を削除すれば即動く
つまり、Vercel は Next.js が求める機能すべてをそのまま提供できる。
あなたの構成に対しては
- SSR OK
- サーバーファンクション OK
- API Routes OK
- no-store OK
- 環境変数をサーバーだけに安全に保持 OK
…全部まるっと対応している。
要するに、
Cloudflare Pages は“静的向けの弱いモード”、Vercel は“Next.js向けの完全仕様”
という違いです。
余談 Cloudflare でも動かす方法はあった?
可能です。ただし Cloudflare Pages + Functions(Workers) を使う必要があり、設定は複雑になります。
例:
- Next.js のサーバーコンポーネントを Cloudflare の Workers Runtime で動かす
- output: "export" を捨てて Functions を有効化
- API Routes を Workers Functions に変換
- Edge runtime 互換の fetch のみ使用
正直、初心者がやるには工程が多い。
Vercel の方がスマートすぎるので、今回の判断は最適です。
エラーまとめ
- Cloudflare Pages → 基本は静的ホスティング。SSR や API Routes が弱い
- あなたの Next.js プロジェクト → SSR / API Routes / 動的フェッチ 必須
- そのため Cloudflare Pages と噛み合わずビルドエラー
- Vercel → Next.js の本場で SSR/API 含めフルサポート → そのまま動く
最終改善点
①next.config.js を修正
output: 'export'
を削除
②トップページ(app/page.tsx)のフェッチ方法を修正
変更前
fetch("/api/apod?count=1")
変更後
fetch("/api/apod?latest=1")
運用版
const res = await fetch("/api/apod?latest=1", { cache: "no-store" });
③route.tsも併せて修正
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const API_KEY = process.env.NASA_API_KEY;
if (!API_KEY) {
return NextResponse.json({ error: "NASA APIキー未設定" }, { status: 500 });
}
const latest = req.nextUrl.searchParams.get("latest");
const count = Number(req.nextUrl.searchParams.get("count") || 1);
if (latest) {
const res = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=${API_KEY}&count=10`,
{ cache: "no-store" }
);
const data = await res.json();
data.sort((a: any, b: any) => new Date(b.date) - new Date(a.date));
return NextResponse.json(data[0]);
}
const res = await fetch(
`https://api.nasa.gov/planetary/apod?api_key=${API_KEY}&count=${count}`,
{ cache: "no-store" }
);
const data = await res.json();
if (count === 1) return NextResponse.json(data[0]);
return NextResponse.json(data.filter((d: any) => d && d.url));
}
全修正ポイント(超まとめ)
- next.config から output: "export" を削除
- route.ts に latest モードを追加
- トップページの fetch を latest=1 に変更
- デプロイ先を Cloudflare → Vercel に変更
- NASA_API_KEY を Vercel の環境変数に設定
これで
- ランダム化していたトップページが正しく最新画像に固定
- キャッシュ問題も解消
- サーバー側で安全に NASA API を呼べる
という完全に筋の通った状態になっています。
修正後のサイト
編集後記
- サーバーの選定も必要ってのが十二分に思い知らされた
- また、キャッシュを利用しない=サーバーの通信料が増えるという新しい問題も(API制限とかある場合も)。。。
- コスト意識も考えさせられるいい経験になったと思います。
Discussion