個人開発で美容・サプリのコスパ比較メディア(mirucos)を Next.js 14 App Router + Supabase + Vercel で作っています。その中で地味に時間を溶かした3つの罠と解決法をメモします。同じ構成の人の役に立てば。
1. App Routerで sitemap.xml が404になる(generateSitemaps の罠)
app/sitemap.ts で generateSitemaps() を使うと、実際に配信されるのは /sitemap/0.xml などのシャード版で、/sitemap.xml 自体は 404 になります。robots.txt から /sitemap.xml を参照していると、Search Consoleで「サイトマップを取得できませんでした」になって地味にハマります。
URL数が5万未満なら単一サイトマップで十分なので、generateSitemaps を使わず素直に1ファイルにします。
ts// app/sitemap.ts
import type { MetadataRoute } from "next";
export const revalidate = 3600;
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const base = "https://example.com";
const slugs = await getPublishedSlugs(); // DBから取得
return [
{ url: `${base}/`, changeFrequency: "weekly", priority: 1 },
...slugs.map((s) => ({ url: `${base}/product/${s}`, priority: 0.7 })),
];
}
```
これで `/sitemap.xml` が `200 / application/xml` で返るようになります。
## 2. Supabase(PostgREST)の「あいまいembed」エラー(PGRST201)
2つのテーブル間に**外部キーが2本**あると、ネストした `select` がどのFK経由か曖昧になり `PGRST201` で落ちます。
例:`articles.product_id → products.id` と `products.article_id → articles.id` の相互参照があるケース。
```ts
// ❌ あいまいでエラー(Could not embed because more than one relationship was found)
client.from("articles").select("*, products(*)");
// ✅ FK名を明示すれば解決
client.from("articles").select("*, product:products!articles_product_id_fkey(*)");
```
`!inner` を付ければINNER JOIN相当になり、`products` 側カラムでのフィルタも効きます。
```ts
client
.from("articles")
.select("*, product:products!articles_product_id_fkey!inner(*)")
.eq("products.category_slug", "skincare");
```
## 3. 外部リンク(アフィリンク等)の遷移を `preconnect` で体感高速化
外部ドメインへの遷移は毎回 DNS解決 + TCP + TLS が走って待たされます。**ホバーした時点で宛先オリジンへ `preconnect` を打っておく**と、クリック時にはハンドシェイク済みで体感がかなり変わります。
```tsx
// ホバー/フォーカスでpreconnectを動的注入(簡略版)
function warmOrigin(origin: string) {
for (const rel of ["preconnect", "dns-prefetch"]) {
const l = document.createElement("link");
l.rel = rel;
l.href = origin;
document.head.appendChild(l);
}
}
```
- 内部リンク:`router.prefetch(path)` でRSCを先読み
- - 外部リンク:宛先オリジンへ `preconnect` / `dns-prefetch`
と使い分けると、回遊も外部遷移も軽くなります。よく使う外部ドメインは `app/layout.tsx` の `<head>` に静的に `preconnect` を置いておくのも有効です。
## おまけ:TailwindでCSS変数カラー + 不透明度が効かない
`--brand` のようなCSS変数で定義した色に対して `bg-brand/20` のようなalpha修飾子を付けても**描画されない**ことがあります(変数の形式次第)。素の色トークン(`amber-50` 等)を使うか、`<alpha-value>` を仕込んだ定義にするのが無難でした。
---
以上、Next.js App Router + Supabase で個人開発する人の時短になれば幸いです。実際に動かしているサイトは [mirucos(コスメ・サプリのコスパ比較メディア)](https://mirucos.com) です。何か間違いや改善点があればコメントで教えてください🙏