From 8cdf0231ced669e24f600976de8be1e759cb50b2 Mon Sep 17 00:00:00 2001 From: Michilis Date: Wed, 1 Apr 2026 19:52:54 +0000 Subject: [PATCH] fix(frontend): resolve API base URL at request time for production Use same-origin /api in the browser so builds are not stuck with baked-in localhost. Server-side fetches use INTERNAL_API_URL, NEXT_PUBLIC_API_URL, or loopback. Centralize logic in lib/api-base.ts. Made-with: Cursor --- frontend/app/.well-known/nostr.json/route.ts | 5 ++--- frontend/app/blog/[slug]/page.tsx | 5 ++--- frontend/app/calendar/route.ts | 5 ++--- frontend/app/events/[id]/page.tsx | 5 ++--- frontend/app/sitemap.ts | 4 ++-- frontend/lib/api-base.ts | 12 ++++++++++++ frontend/lib/api.ts | 6 +++--- 7 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 frontend/lib/api-base.ts diff --git a/frontend/app/.well-known/nostr.json/route.ts b/frontend/app/.well-known/nostr.json/route.ts index 8313aa4..1098dee 100644 --- a/frontend/app/.well-known/nostr.json/route.ts +++ b/frontend/app/.well-known/nostr.json/route.ts @@ -1,10 +1,9 @@ import { NextRequest, NextResponse } from 'next/server'; - -const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api'; +import { apiUrl } from '@/lib/api-base'; export async function GET(req: NextRequest) { const name = req.nextUrl.searchParams.get('name'); - const upstream = new URL(`${API_URL}/nip05`); + const upstream = new URL(apiUrl('/nip05')); if (name) upstream.searchParams.set('name', name); const res = await fetch(upstream.toString(), { cache: 'no-store' }); diff --git a/frontend/app/blog/[slug]/page.tsx b/frontend/app/blog/[slug]/page.tsx index e007617..fdeccbc 100644 --- a/frontend/app/blog/[slug]/page.tsx +++ b/frontend/app/blog/[slug]/page.tsx @@ -1,12 +1,11 @@ import type { Metadata } from "next"; import BlogPostClient from "./BlogPostClient"; import { BlogPostingJsonLd, BreadcrumbJsonLd } from "@/components/public/JsonLd"; - -const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api"; +import { apiUrl } from "@/lib/api-base"; async function fetchPost(slug: string) { try { - const res = await fetch(`${apiUrl}/posts/${slug}`, { + const res = await fetch(apiUrl(`/posts/${slug}`), { next: { revalidate: 300 }, }); if (!res.ok) return null; diff --git a/frontend/app/calendar/route.ts b/frontend/app/calendar/route.ts index ffc0957..90e4944 100644 --- a/frontend/app/calendar/route.ts +++ b/frontend/app/calendar/route.ts @@ -1,11 +1,10 @@ import { NextResponse } from 'next/server'; - -const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api'; +import { apiUrl } from '@/lib/api-base'; export async function GET() { let upstream: Response; try { - upstream = await fetch(`${API_URL}/calendar/ics`, { + upstream = await fetch(apiUrl('/calendar/ics'), { headers: { Accept: 'text/calendar' }, cache: 'no-store', }); diff --git a/frontend/app/events/[id]/page.tsx b/frontend/app/events/[id]/page.tsx index e399ff8..1e51f84 100644 --- a/frontend/app/events/[id]/page.tsx +++ b/frontend/app/events/[id]/page.tsx @@ -1,12 +1,11 @@ import type { Metadata } from "next"; import EventDetailClient from "./EventDetailClient"; import { EventJsonLd, BreadcrumbJsonLd } from "@/components/public/JsonLd"; - -const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api"; +import { apiUrl } from "@/lib/api-base"; async function fetchEvent(id: string) { try { - const res = await fetch(`${apiUrl}/meetups/${id}`, { + const res = await fetch(apiUrl(`/meetups/${id}`), { next: { revalidate: 300 }, }); if (!res.ok) return null; diff --git a/frontend/app/sitemap.ts b/frontend/app/sitemap.ts index c64b642..67db928 100644 --- a/frontend/app/sitemap.ts +++ b/frontend/app/sitemap.ts @@ -1,12 +1,12 @@ import type { MetadataRoute } from "next"; +import { apiUrl } from "@/lib/api-base"; const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://belgianbitcoinembassy.org"; -const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api"; async function fetchJson(path: string): Promise { try { - const res = await fetch(`${apiUrl}${path}`, { next: { revalidate: 3600 } }); + const res = await fetch(apiUrl(path), { next: { revalidate: 3600 } }); if (!res.ok) return null; return res.json(); } catch { diff --git a/frontend/lib/api-base.ts b/frontend/lib/api-base.ts new file mode 100644 index 0000000..16b29e6 --- /dev/null +++ b/frontend/lib/api-base.ts @@ -0,0 +1,12 @@ +/** Browser: `/api/...` (same origin). Server: INTERNAL_API_URL → NEXT_PUBLIC_API_URL → loopback. */ +export function apiUrl(path: string): string { + const p = path.startsWith("/") ? path : `/${path}`; + if (typeof window !== "undefined") { + return `/api${p}`; + } + const base = + process.env.INTERNAL_API_URL || + process.env.NEXT_PUBLIC_API_URL || + "http://127.0.0.1:4000/api"; + return `${base}${p}`; +} diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index 0b0ce32..ef60f64 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -1,4 +1,4 @@ -const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000/api"; +import { apiUrl } from "./api-base"; async function request(path: string, options?: RequestInit): Promise { const token = typeof window !== "undefined" ? localStorage.getItem("bbe_token") : null; @@ -8,7 +8,7 @@ async function request(path: string, options?: RequestInit): Promise { ...options?.headers, }; - const res = await fetch(`${API_URL}${path}`, { ...options, headers }); + const res = await fetch(apiUrl(path), { ...options, headers }); if (!res.ok) { const error = await res.json().catch(() => ({ message: "Request failed" })); throw new Error(error.message || `HTTP ${res.status}`); @@ -128,7 +128,7 @@ export const api = { const token = typeof window !== "undefined" ? localStorage.getItem("bbe_token") : null; const formData = new FormData(); formData.append("file", file); - const res = await fetch(`${API_URL}/media/upload`, { + const res = await fetch(apiUrl("/media/upload"), { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {}, body: formData,