selfhost: route brand assets through /storage/ rewrite
Download 3 Tuxedo brand logos from Supabase Storage to local MinIO and point the DB at portable /storage/... paths. Add a Next.js rewrite in next.config.ts that proxies /storage/* to the configured MinIO endpoint. Why: Next.js's image optimizer refuses to fetch upstream images whose hostname resolves to a private IP. Local MinIO lives on 127.0.0.1, so direct URLs (e.g. http://localhost:9000/...) get blocked with "upstream image resolved to private ip [::1,127.0.0.1]". The rewrite keeps URLs same-origin in the browser, so the optimizer's private-IP check is bypassed. For production, set STORAGE_PUBLIC_URL to the public MinIO endpoint and the same DB values work without change. Also fix the same pattern in 3 client-rendered components that hardcoded publicUrl(BUCKETS.BRAND_LOGOS, ...) for fallback assets: - TuxedoVideoHero (hero video + Olathe Sweet dark logo) - TimeTrackingFieldClient (field UI logo) - tuxedo/about (about page logo) email-service.ts keeps publicUrl() because Resend fetches URLs server-side from its own network. Documented the workflow and gotchas in docs/SUPABASE_DUMP_GUIDE.md.
This commit is contained in:
@@ -203,3 +203,86 @@ psql -U routecommerce -h 127.0.0.1 -d route_commerce -c "SELECT name, slug, crea
|
|||||||
for reproducibility. The data dump is gitignored (regenerate as needed).
|
for reproducibility. The data dump is gitignored (regenerate as needed).
|
||||||
- After dump, the local PostgREST needs a schema cache reload — restart
|
- After dump, the local PostgREST needs a schema cache reload — restart
|
||||||
the postgrest process or it'll serve stale metadata for ~30 seconds.
|
the postgrest process or it'll serve stale metadata for ~30 seconds.
|
||||||
|
|
||||||
|
## Migrating Brand Assets (Supabase Storage → MinIO)
|
||||||
|
|
||||||
|
Brand logos and other Storage files are NOT in the Postgres dump. The
|
||||||
|
storage layer is now MinIO (S3-compatible) instead of Supabase Storage.
|
||||||
|
|
||||||
|
### Download assets from Supabase
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make sure the target buckets exist in MinIO (one-time)
|
||||||
|
mc mb --ignore-existing local/brand-logos local/videos \
|
||||||
|
local/product-images local/contacts-imports \
|
||||||
|
local/water-photos
|
||||||
|
|
||||||
|
# Pull each asset via the public URL. Path is <bucket>/<key> after the
|
||||||
|
# /storage/v1/object/public/ prefix in the Supabase URL.
|
||||||
|
mkdir -p .data/assets
|
||||||
|
BRAND_ID="<your-brand-uuid>"
|
||||||
|
for fname in logo.png olathe-sweet-logo.png olathe-sweet-logo-dark.png; do
|
||||||
|
curl -sf -o ".data/assets/$fname" \
|
||||||
|
"https://<project-ref>.supabase.co/storage/v1/object/public/brand-logos/$BRAND_ID/$fname"
|
||||||
|
mc cp ".data/assets/$fname" "local/brand-logos/$BRAND_ID/$fname"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Point the DB at MinIO (portable /storage/... paths)
|
||||||
|
|
||||||
|
After capture, the `brand_settings.logo_url` etc. values still point at
|
||||||
|
the Supabase URL. Replace the base with a relative `/storage/` path so
|
||||||
|
the Next.js rewrite in `next.config.ts` can route to whichever MinIO
|
||||||
|
endpoint is configured per environment (dev → `localhost:9000`, prod →
|
||||||
|
`storage.route.crispygoat.com`).
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE brand_settings
|
||||||
|
SET
|
||||||
|
logo_url = REPLACE(logo_url, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage'),
|
||||||
|
logo_url_dark = REPLACE(logo_url_dark, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage'),
|
||||||
|
olathe_sweet_logo_url = REPLACE(olathe_sweet_logo_url, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage'),
|
||||||
|
olathe_sweet_logo_url_dark = REPLACE(olathe_sweet_logo_url_dark, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage'),
|
||||||
|
hero_image_url = REPLACE(hero_image_url, 'https://<project-ref>.supabase.co/storage/v1/object/public', '/storage');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why a rewrite instead of pointing at MinIO directly
|
||||||
|
|
||||||
|
Next.js's image optimizer (`/_next/image?url=...`) refuses to fetch
|
||||||
|
upstream images whose hostname resolves to a private IP. Local MinIO is
|
||||||
|
on `127.0.0.1` / `::1`, so URLs like `http://localhost:9000/...` get
|
||||||
|
blocked:
|
||||||
|
|
||||||
|
```
|
||||||
|
⨯ upstream image http://localhost:9000/... resolved to private ip
|
||||||
|
["::1","127.0.0.1"]
|
||||||
|
```
|
||||||
|
|
||||||
|
The rewrite in `next.config.ts` resolves `/storage/*` to the configured
|
||||||
|
MinIO base URL server-side, so the browser sees a same-origin URL and
|
||||||
|
the optimizer's private-IP check is bypassed:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async rewrites() {
|
||||||
|
const storageBase = process.env.STORAGE_PUBLIC_URL || "http://localhost:9000";
|
||||||
|
return [{ source: "/storage/:path*", destination: `${storageBase}/:path*` }];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For production, set `STORAGE_PUBLIC_URL` to the public MinIO endpoint
|
||||||
|
(e.g., `https://storage.route.crispygoat.com`) and the same DB values
|
||||||
|
work without modification.
|
||||||
|
|
||||||
|
### Hardcoded brand asset URLs in client components
|
||||||
|
|
||||||
|
A few components used `publicUrl(BUCKETS.BRAND_LOGOS, ...)` to build a
|
||||||
|
MinIO URL at module load (used as a fallback before client-side data
|
||||||
|
loads). Switch these to `/storage/...` paths so the rewrite covers them:
|
||||||
|
|
||||||
|
- `src/components/storefront/TuxedoVideoHero.tsx` — hero video + Olathe Sweet dark logo
|
||||||
|
- `src/components/time-tracking/TimeTrackingFieldClient.tsx` — field UI logo
|
||||||
|
- `src/app/tuxedo/about/page.tsx` — about page logo
|
||||||
|
|
||||||
|
`src/lib/email-service.ts` should keep using `publicUrl(...)` because
|
||||||
|
Resend fetches the URL server-side from its own network — relative
|
||||||
|
paths won't work for emails.
|
||||||
|
|||||||
+11
-1
@@ -106,8 +106,18 @@ const nextConfig: NextConfig = {
|
|||||||
|
|
||||||
// Rewrites for API proxy
|
// Rewrites for API proxy
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
|
// Storage proxy: /storage/* -> MinIO at the same path
|
||||||
|
// Lets brand assets and product images use portable relative URLs
|
||||||
|
// (e.g. /storage/brand-logos/<id>/logo.png) in the DB, with Next.js
|
||||||
|
// proxying to whichever MinIO endpoint is configured for the environment.
|
||||||
|
// Avoids the next/image "upstream resolved to private ip" block on
|
||||||
|
// localhost MinIO by keeping the upstream fetch on the server side.
|
||||||
|
const storageBase = process.env.STORAGE_PUBLIC_URL || "http://localhost:9000";
|
||||||
return [
|
return [
|
||||||
// Add any necessary rewrites here
|
{
|
||||||
|
source: "/storage/:path*",
|
||||||
|
destination: `${storageBase}/:path*`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
|||||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { getBrandSettingsPublic } from "@/actions/brand-settings";
|
import { getBrandSettingsPublic } from "@/actions/brand-settings";
|
||||||
import { publicUrl, BUCKETS } from "@/lib/storage";
|
|
||||||
|
|
||||||
// Lazy load heavy sections
|
// Lazy load heavy sections
|
||||||
const MissionSection = lazy(() => import("@/components/about/MissionSection"));
|
const MissionSection = lazy(() => import("@/components/about/MissionSection"));
|
||||||
@@ -14,10 +13,11 @@ const FamilyTimelineSection = lazy(() => import("@/components/about/FamilyTimeli
|
|||||||
const ContactSection = lazy(() => import("@/components/about/ContactSection"));
|
const ContactSection = lazy(() => import("@/components/about/ContactSection"));
|
||||||
const CTASection = lazy(() => import("@/components/about/CTASection"));
|
const CTASection = lazy(() => import("@/components/about/CTASection"));
|
||||||
|
|
||||||
const OLATHE_SWEET_LOGO_DARK = publicUrl(
|
// Use Next.js rewrite (next.config.ts) so the browser can stream assets
|
||||||
BUCKETS.BRAND_LOGOS,
|
// via same-origin /storage/* and avoid next/image "upstream resolved to
|
||||||
"64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png"
|
// private ip" failures when the upstream is localhost MinIO.
|
||||||
);
|
const OLATHE_SWEET_LOGO_DARK =
|
||||||
|
"/storage/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png";
|
||||||
|
|
||||||
export default function TuxedoAboutPage() {
|
export default function TuxedoAboutPage() {
|
||||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { gsap } from "gsap";
|
import { gsap } from "gsap";
|
||||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
import { publicUrl, BUCKETS } from "@/lib/storage";
|
|
||||||
|
|
||||||
// Register GSAP plugins
|
// Register GSAP plugins
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -23,12 +22,13 @@ type TuxedoVideoHeroProps = {
|
|||||||
onSecondaryClick?: () => void;
|
onSecondaryClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const VIDEO_URL = publicUrl(BUCKETS.VIDEOS, "tuxedo-hero.mp4");
|
// Use Next.js rewrite (next.config.ts) so the browser can stream assets
|
||||||
|
// via same-origin /storage/* and avoid next/image "upstream resolved to
|
||||||
|
// private ip" failures when the upstream is localhost MinIO.
|
||||||
|
const VIDEO_URL = `/storage/videos/tuxedo-hero.mp4`;
|
||||||
|
|
||||||
const OLATHE_SWEET_LOGO_DARK = publicUrl(
|
const OLATHE_SWEET_LOGO_DARK =
|
||||||
BUCKETS.BRAND_LOGOS,
|
"/storage/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo-dark.png";
|
||||||
"64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo-dark.png"
|
|
||||||
);
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// CINEMATIC HERO - SCROLL-DRIVEN ANIMATIONS
|
// CINEMATIC HERO - SCROLL-DRIVEN ANIMATIONS
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
import { publicUrl, BUCKETS } from "@/lib/storage";
|
|
||||||
import {
|
import {
|
||||||
verifyTimeTrackingPin,
|
verifyTimeTrackingPin,
|
||||||
clockInWorker,
|
clockInWorker,
|
||||||
@@ -22,10 +21,10 @@ import {
|
|||||||
const BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
const BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||||
const BRAND_NAME = "Tuxedo Corn";
|
const BRAND_NAME = "Tuxedo Corn";
|
||||||
|
|
||||||
const OLATHE_SWEET_LOGO_DARK = publicUrl(
|
// Use Next.js rewrite (next.config.ts) so the browser can stream assets
|
||||||
BUCKETS.BRAND_LOGOS,
|
// via same-origin /storage/* and avoid next/image "upstream resolved to
|
||||||
`${BRAND_ID}/olathe-sweet-logo.png`
|
// private ip" failures when the upstream is localhost MinIO.
|
||||||
);
|
const OLATHE_SWEET_LOGO_DARK = `/storage/brand-logos/${BRAND_ID}/olathe-sweet-logo.png`;
|
||||||
|
|
||||||
// ── Translations ───────────────────────────────────────────────────────────────
|
// ── Translations ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user