🛰️

Next.js で NASA の APOD API を使って最新画像を表示した話

に公開

きっかけ

  • もともとポートフォリオとしてNASAの宇宙写真が表示されるページをNext.jsで作成
  • topは最新の画像が1枚、archiveは過去の画像からランダムに7枚表示
  • しかし、再度見たら全然ランダムじゃない!
  • それどころか作成段階から最新画像が更新されない!

今回行ったこと

  • 取得・表示コードの修正
  • APIルートの作成
  • cloudflarePagesからVercelに移行

原因

  • ホスティング
  • API取得の仕組みを理解していなかった

元サイト

https://next-portfolio-6xl.pages.dev/

環境

$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 を呼べる
    という完全に筋の通った状態になっています。

修正後のサイト

https://next-portfolio-ecru-psi.vercel.app/

編集後記

  • サーバーの選定も必要ってのが十二分に思い知らされた
  • また、キャッシュを利用しない=サーバーの通信料が増えるという新しい問題も(API制限とかある場合も)。。。
  • コスト意識も考えさせられるいい経験になったと思います。

次回、翻訳+音声読み上げを導入しました

Discussion