diff --git a/.env.example b/.env.example index 1287b316..e4b64dd1 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,7 @@ STELLAR_NETWORK=testnet CONTRACT_ADDRESS= BACKEND_URL=http://localhost:3001 JWT_SECRET=your_jwt_secret_key +NEXT_PUBLIC_IMAGE_REMOTE_PATTERNS=https://cdn.myfans.app,https://media.myfans.app # ----------------------------------------------------------------------------- # CORS Configuration (for frontend reference) diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 49872cd2..100a1116 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,13 +1,12 @@ import type { NextConfig } from "next"; +import { getRemoteImagePatterns } from "./src/lib/image-remote-patterns"; const nextConfig: NextConfig = { turbopack: { root: process.cwd(), }, images: { - remotePatterns: [ - { protocol: 'https', hostname: '**' }, - ], + remotePatterns: getRemoteImagePatterns(), }, // Ensure proper metadata handling for SSR/SSG experimental: { diff --git a/frontend/src/lib/image-remote-patterns.test.ts b/frontend/src/lib/image-remote-patterns.test.ts new file mode 100644 index 00000000..9626a3c3 --- /dev/null +++ b/frontend/src/lib/image-remote-patterns.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { getRemoteImagePatterns } from "./image-remote-patterns"; + +describe("getRemoteImagePatterns", () => { + it("returns localhost defaults when no env override is configured", () => { + expect(getRemoteImagePatterns(undefined)).toEqual([ + { protocol: "http", hostname: "localhost", pathname: "/**" }, + { protocol: "http", hostname: "127.0.0.1", pathname: "/**" }, + ]); + }); + + it("parses comma-separated hostnames as https image hosts", () => { + expect(getRemoteImagePatterns("cdn.myfans.app, media.myfans.app")).toEqual([ + { protocol: "http", hostname: "localhost", pathname: "/**" }, + { protocol: "http", hostname: "127.0.0.1", pathname: "/**" }, + { protocol: "https", hostname: "cdn.myfans.app", pathname: "/**" }, + { protocol: "https", hostname: "media.myfans.app", pathname: "/**" }, + ]); + }); + + it("parses full URLs with protocol, port, and path", () => { + expect( + getRemoteImagePatterns("https://cdn.myfans.app/assets, http://localhost:4000/uploads"), + ).toEqual([ + { protocol: "http", hostname: "localhost", pathname: "/**" }, + { protocol: "http", hostname: "127.0.0.1", pathname: "/**" }, + { protocol: "https", hostname: "cdn.myfans.app", pathname: "/assets/**" }, + { protocol: "http", hostname: "localhost", port: "4000", pathname: "/uploads/**" }, + ]); + }); + + it("deduplicates repeated entries", () => { + expect(getRemoteImagePatterns("localhost,cdn.myfans.app,cdn.myfans.app")).toEqual([ + { protocol: "http", hostname: "localhost", pathname: "/**" }, + { protocol: "http", hostname: "127.0.0.1", pathname: "/**" }, + { protocol: "https", hostname: "cdn.myfans.app", pathname: "/**" }, + ]); + }); +}); diff --git a/frontend/src/lib/image-remote-patterns.ts b/frontend/src/lib/image-remote-patterns.ts new file mode 100644 index 00000000..e9d6be98 --- /dev/null +++ b/frontend/src/lib/image-remote-patterns.ts @@ -0,0 +1,59 @@ +import type { RemotePattern } from "next/dist/shared/lib/image-config"; + +const DEFAULT_REMOTE_IMAGE_HOSTS = ["http://localhost", "http://127.0.0.1"]; +const DEFAULT_PROTOCOL = "https"; + +function normalizeEntry(entry: string): string | null { + const trimmed = entry.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function toPattern(entry: string): RemotePattern { + if (entry === "localhost" || entry === "127.0.0.1") { + return { + protocol: "http", + hostname: entry, + pathname: "/**", + }; + } + + if (entry.includes("://")) { + const url = new URL(entry); + + return { + protocol: url.protocol.replace(":", "") as RemotePattern["protocol"], + hostname: url.hostname, + ...(url.port ? { port: url.port } : {}), + pathname: url.pathname && url.pathname !== "/" ? `${url.pathname.replace(/\/$/, "")}/**` : "/**", + }; + } + + return { + protocol: DEFAULT_PROTOCOL, + hostname: entry, + pathname: "/**", + }; +} + +export function getRemoteImagePatterns( + rawValue = process.env.NEXT_PUBLIC_IMAGE_REMOTE_PATTERNS, +): RemotePattern[] { + const configuredEntries = + rawValue + ?.split(",") + .map(normalizeEntry) + .filter((entry): entry is string => entry !== null) ?? []; + + const uniqueEntries = [...new Set([...DEFAULT_REMOTE_IMAGE_HOSTS, ...configuredEntries])]; + const uniquePatterns = new Map(); + + for (const entry of uniqueEntries) { + const pattern = toPattern(entry); + const key = [pattern.protocol, pattern.hostname, pattern.port ?? "", pattern.pathname ?? ""].join("|"); + if (!uniquePatterns.has(key)) { + uniquePatterns.set(key, pattern); + } + } + + return [...uniquePatterns.values()]; +}