Use R2 for public image assets
This commit is contained in:
@@ -27,3 +27,8 @@ AUTH_URL=http://localhost:3000
|
|||||||
# https://console.cloud.google.com/apis/credentials
|
# https://console.cloud.google.com/apis/credentials
|
||||||
AUTH_GOOGLE_ID=
|
AUTH_GOOGLE_ID=
|
||||||
AUTH_GOOGLE_SECRET=
|
AUTH_GOOGLE_SECRET=
|
||||||
|
|
||||||
|
# Cloudflare R2 public image origin
|
||||||
|
# Use a custom domain or R2 public development URL with no trailing slash.
|
||||||
|
# Upload site assets under /images, for example /images/hero-rv-sunset.jpg.
|
||||||
|
NEXT_PUBLIC_R2_PUBLIC_URL=
|
||||||
|
|||||||
@@ -1,8 +1,40 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
function getR2PublicUrl(): URL | null {
|
||||||
|
const publicUrl = process.env.NEXT_PUBLIC_R2_PUBLIC_URL?.trim();
|
||||||
|
|
||||||
|
if (!publicUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(publicUrl);
|
||||||
|
|
||||||
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const r2PublicUrl = getR2PublicUrl();
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [],
|
remotePatterns: r2PublicUrl
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
protocol: r2PublicUrl.protocol.replace(":", "") as "http" | "https",
|
||||||
|
hostname: r2PublicUrl.hostname,
|
||||||
|
port: r2PublicUrl.port,
|
||||||
|
pathname: "/**",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
unoptimized: Boolean(r2PublicUrl),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import SectionWrapper from "@/components/ui/SectionWrapper";
|
import SectionWrapper from "@/components/ui/SectionWrapper";
|
||||||
import Button from "@/components/ui/Button";
|
import Button from "@/components/ui/Button";
|
||||||
|
import { assetUrl } from "@/lib/assets";
|
||||||
|
|
||||||
export default function FounderSection() {
|
export default function FounderSection() {
|
||||||
return (
|
return (
|
||||||
@@ -10,7 +11,7 @@ export default function FounderSection() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative rounded-2xl overflow-hidden aspect-[4/5] bg-slate-100 shadow-xl">
|
<div className="relative rounded-2xl overflow-hidden aspect-[4/5] bg-slate-100 shadow-xl">
|
||||||
<Image
|
<Image
|
||||||
src="/images/david.jpg"
|
src={assetUrl("/images/david.jpg")}
|
||||||
alt="David, Founder of TechSolve Travel, working from his RV with a Starlink and Peplink setup"
|
alt="David, Founder of TechSolve Travel, working from his RV with a Starlink and Peplink setup"
|
||||||
fill
|
fill
|
||||||
className="object-cover object-top"
|
className="object-cover object-top"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Button from "@/components/ui/Button";
|
import Button from "@/components/ui/Button";
|
||||||
|
import { assetUrl } from "@/lib/assets";
|
||||||
|
|
||||||
export default function HeroSection() {
|
export default function HeroSection() {
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-[90vh] flex items-center">
|
<section className="relative min-h-[90vh] flex items-center">
|
||||||
{/* Background image */}
|
{/* Background image */}
|
||||||
<Image
|
<Image
|
||||||
src="/images/hero-rv-sunset.jpg"
|
src={assetUrl("/images/hero-rv-sunset.jpg")}
|
||||||
alt="RV parked at sunset with Starlink dish and enterprise networking hardware"
|
alt="RV parked at sunset with Starlink dish and enterprise networking hardware"
|
||||||
fill
|
fill
|
||||||
priority
|
priority
|
||||||
|
|||||||
33
src/lib/assets.ts
Normal file
33
src/lib/assets.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const assetBaseUrl = getAssetBaseUrl();
|
||||||
|
|
||||||
|
function getAssetBaseUrl(): string {
|
||||||
|
const rawUrl = process.env.NEXT_PUBLIC_R2_PUBLIC_URL?.trim();
|
||||||
|
|
||||||
|
if (!rawUrl) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(rawUrl);
|
||||||
|
|
||||||
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
url.search = "";
|
||||||
|
url.hash = "";
|
||||||
|
|
||||||
|
return url.href.replace(/\/$/, "");
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assetUrl(path: string): string {
|
||||||
|
if (!assetBaseUrl) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||||
|
return `${assetBaseUrl}${normalizedPath}`;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PricingTier, ProcessStep, TrustLogo } from "@/types";
|
import type { PricingTier, ProcessStep, TrustLogo } from "@/types";
|
||||||
|
import { assetUrl } from "@/lib/assets";
|
||||||
|
|
||||||
export const PROCESS_STEPS: ProcessStep[] = [
|
export const PROCESS_STEPS: ProcessStep[] = [
|
||||||
{
|
{
|
||||||
@@ -98,34 +99,39 @@ export const PRICING_TIERS: PricingTier[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const TRUST_LOGOS: TrustLogo[] = [
|
export const TRUST_LOGOS: TrustLogo[] = [
|
||||||
{ name: "Peplink", src: "/images/logo-peplink.svg", width: 120, height: 40 },
|
{
|
||||||
|
name: "Peplink",
|
||||||
|
src: assetUrl("/images/logo-peplink.svg"),
|
||||||
|
width: 120,
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "GL.iNet",
|
name: "GL.iNet",
|
||||||
src: "/images/logo-gliinet.svg",
|
src: assetUrl("/images/logo-gliinet.svg"),
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 40,
|
height: 40,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ubiquiti UniFi",
|
name: "Ubiquiti UniFi",
|
||||||
src: "/images/logo-ubiquiti-unifi.svg",
|
src: assetUrl("/images/logo-ubiquiti-unifi.svg"),
|
||||||
width: 110,
|
width: 110,
|
||||||
height: 40,
|
height: 40,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "WireGuard",
|
name: "WireGuard",
|
||||||
src: "/images/logo-wireguard.svg",
|
src: assetUrl("/images/logo-wireguard.svg"),
|
||||||
width: 120,
|
width: 120,
|
||||||
height: 40,
|
height: 40,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Fortinet",
|
name: "Fortinet",
|
||||||
src: "/images/logo-fortinet.svg",
|
src: assetUrl("/images/logo-fortinet.svg"),
|
||||||
width: 110,
|
width: 110,
|
||||||
height: 40,
|
height: 40,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Starlink",
|
name: "Starlink",
|
||||||
src: "/images/logo-starlink.svg",
|
src: assetUrl("/images/logo-starlink.svg"),
|
||||||
width: 110,
|
width: 110,
|
||||||
height: 40,
|
height: 40,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user