diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index e69ba84..3d22905 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -12,10 +12,14 @@ COPY . .
# Build args
ARG DIRECTUS_API_TOKEN
+ARG NEXT_PUBLIC_DIRECTUS_ASSET_URL=http://katheryn-cms:8055
+ARG NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN
# Set environment for build
ENV NEXT_PUBLIC_DIRECTUS_URL=https://katheryn-cms.jeffemmett.com
ENV DIRECTUS_API_TOKEN=${DIRECTUS_API_TOKEN}
+ENV NEXT_PUBLIC_DIRECTUS_ASSET_URL=${NEXT_PUBLIC_DIRECTUS_ASSET_URL}
+ENV NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN=${NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN}
ENV NEXT_TELEMETRY_DISABLED=1
# Build the application
diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml
index 1f152ff..507d7c1 100644
--- a/frontend/docker-compose.yml
+++ b/frontend/docker-compose.yml
@@ -5,6 +5,8 @@ services:
dockerfile: Dockerfile
args:
- DIRECTUS_API_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
+ - NEXT_PUBLIC_DIRECTUS_ASSET_URL=http://katheryn-cms:8055
+ - NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN=katheryn-frontend-readonly-8591de0316ded82fab45328cf1e49cb1
container_name: katheryn-frontend
restart: unless-stopped
env_file:
diff --git a/frontend/next.config.ts b/frontend/next.config.ts
index 05ec20f..e06fa5e 100644
--- a/frontend/next.config.ts
+++ b/frontend/next.config.ts
@@ -15,6 +15,12 @@ const nextConfig: NextConfig = {
port: '8055',
pathname: '/assets/**',
},
+ {
+ protocol: 'http',
+ hostname: 'katheryn-cms',
+ port: '8055',
+ pathname: '/assets/**',
+ },
{
protocol: 'https',
hostname: 'images.squarespace-cdn.com',
diff --git a/frontend/src/app/store/[slug]/artwork-detail-client.tsx b/frontend/src/app/store/[slug]/artwork-detail-client.tsx
new file mode 100644
index 0000000..5804a4e
--- /dev/null
+++ b/frontend/src/app/store/[slug]/artwork-detail-client.tsx
@@ -0,0 +1,259 @@
+'use client';
+
+import { useState } from 'react';
+import Image from 'next/image';
+import Link from 'next/link';
+import { Artwork, getAssetUrl } from '@/lib/directus';
+import { useCart } from '@/context/cart-context';
+
+interface ArtworkDetailClientProps {
+ artwork: Artwork;
+ relatedWorks: Artwork[];
+ mainImageUrl: string;
+ galleryImageUrls: string[];
+ thumbnailUrl: string;
+}
+
+export default function ArtworkDetailClient({
+ artwork,
+ relatedWorks,
+ mainImageUrl,
+ galleryImageUrls,
+ thumbnailUrl,
+}: ArtworkDetailClientProps) {
+ const [selectedImage, setSelectedImage] = useState(0);
+ const { addItem, isInCart } = useCart();
+ const inCart = isInCart(artwork.id);
+ const isSold = artwork.status === 'sold';
+ const isAvailable = artwork.status === 'published' && artwork.price && artwork.price > 0;
+
+ const handleAddToCart = () => {
+ if (artwork && isAvailable && !inCart) {
+ addItem(artwork);
+ }
+ };
+
+ const galleryImages = artwork.gallery || [];
+
+ return (
+
+ {/* Breadcrumb */}
+
+
+
+
+
+
+ {/* Main content */}
+
+
+ {/* Images */}
+
+
+ {artwork.image ? (
+
+ ) : (
+
+ No image available
+
+ )}
+ {isSold && (
+
+
+ Sold
+
+
+ )}
+
+
+ {/* Thumbnail gallery */}
+ {galleryImages.length > 0 && (
+
+
+ {galleryImageUrls.map((url, index) => (
+
+ ))}
+
+ )}
+
+
+ {/* Details */}
+
+
{artwork.title}
+
+ {artwork.year && (
+
{artwork.year}
+ )}
+
+ {/* Price */}
+
+ {isSold ? (
+
Sold
+ ) : artwork.price ? (
+
{artwork.currency === 'GBP' ? '£' : '$'}{artwork.price.toLocaleString()}
+ ) : (
+
Price on request
+ )}
+
+
+ {/* Add to cart */}
+ {isAvailable && !isSold && (
+
+
+
+ )}
+
+ {/* Enquire button for sold or POA items */}
+ {(isSold || !artwork.price) && (
+
+
+ Enquire About This Work
+
+
+ )}
+
+ {/* Details */}
+
+ {artwork.medium && (
+
+ Medium
+ {artwork.medium}
+
+ )}
+ {artwork.dimensions && (
+
+ Dimensions
+ {artwork.dimensions}
+
+ )}
+ {artwork.series && typeof artwork.series === 'object' && (
+
+ Series
+
+ {artwork.series.name}
+
+
+ )}
+
+
+ {/* Description */}
+ {artwork.description && (
+
+
+ About This Work
+
+
+
+ )}
+
+ {/* Shipping info */}
+
+
+
+
+
Secure Shipping
+
+ All artworks are carefully packaged and shipped with insurance.
+
+
+
+
+
+
+
+
+ {/* Related works */}
+ {relatedWorks.length > 0 && (
+
+
+
You May Also Like
+
+ {relatedWorks.map((work) => (
+
+
+ {work.image && (
+
+ )}
+
+
+
{work.title}
+ {work.price && (
+
{work.currency === 'GBP' ? '£' : '$'}{work.price.toLocaleString()}
+ )}
+
+
+ ))}
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/app/store/[slug]/page.tsx b/frontend/src/app/store/[slug]/page.tsx
index e00fb95..9c82ff3 100644
--- a/frontend/src/app/store/[slug]/page.tsx
+++ b/frontend/src/app/store/[slug]/page.tsx
@@ -1,293 +1,62 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-import Image from 'next/image';
-import Link from 'next/link';
-import { useParams } from 'next/navigation';
+import { Metadata } from 'next';
+import { notFound } from 'next/navigation';
import { getArtwork, getArtworks, getAssetUrl, Artwork } from '@/lib/directus';
-import { useCart } from '@/context/cart-context';
+import ArtworkDetailClient from './artwork-detail-client';
-export default function ArtworkDetailPage() {
- const params = useParams();
- const slug = params.slug as string;
- const [artwork, setArtwork] = useState(null);
- const [relatedWorks, setRelatedWorks] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [selectedImage, setSelectedImage] = useState(0);
+// Force dynamic rendering to ensure fresh data from Directus
+export const dynamic = 'force-dynamic';
+export const revalidate = 0;
- const { addItem, isInCart } = useCart();
- const inCart = artwork ? isInCart(artwork.id) : false;
- const isSold = artwork?.status === 'sold';
- const isAvailable = artwork?.status === 'published' && artwork?.price && artwork.price > 0;
+interface PageProps {
+ params: Promise<{ slug: string }>;
+}
- useEffect(() => {
- async function loadArtwork() {
- try {
- setLoading(true);
- const data = await getArtwork(slug);
- setArtwork(data);
+export async function generateMetadata({ params }: PageProps): Promise {
+ const { slug } = await params;
+ try {
+ const artwork = await getArtwork(slug);
+ return {
+ title: artwork.title,
+ description: artwork.description || `${artwork.title} by Katheryn Trenshaw`,
+ };
+ } catch {
+ return { title: 'Artwork Not Found' };
+ }
+}
- // Fetch related works
- const allWorks = await getArtworks({ status: 'published', limit: 4 });
- setRelatedWorks(allWorks.filter((w) => w.id !== data.id).slice(0, 3));
- } catch (err) {
- setError('Artwork not found');
- console.error(err);
- } finally {
- setLoading(false);
- }
- }
- loadArtwork();
- }, [slug]);
+export default async function ArtworkDetailPage({ params }: PageProps) {
+ const { slug } = await params;
- const handleAddToCart = () => {
- if (artwork && isAvailable && !inCart) {
- addItem(artwork);
- }
- };
-
- if (loading) {
- return (
-
- );
+ let artwork;
+ try {
+ artwork = await getArtwork(slug);
+ } catch {
+ notFound();
}
- if (error || !artwork) {
- return (
-
-
Artwork not found
-
- Back to Store
-
-
- );
+ // Fetch related works server-side
+ let relatedWorks: Artwork[] = [];
+ try {
+ const allWorks = await getArtworks({ status: 'published', limit: 4 });
+ relatedWorks = allWorks.filter((w) => w.id !== artwork.id).slice(0, 3);
+ } catch {
+ relatedWorks = [];
}
+ // Pre-compute image URLs server-side (ensures token is always available)
const mainImageUrl = getAssetUrl(artwork.image, { width: 1200, quality: 90, format: 'webp' });
- const galleryImages = artwork.gallery || [];
+ const thumbnailUrl = getAssetUrl(artwork.image, { width: 200 });
+ const galleryImageUrls = (artwork.gallery || []).map((img) =>
+ getAssetUrl(img, { width: 200 })
+ );
return (
-
- {/* Breadcrumb */}
-
-
-
-
-
-
- {/* Main content */}
-
-
- {/* Images */}
-
-
- {artwork.image ? (
-
- ) : (
-
- No image available
-
- )}
- {isSold && (
-
-
- Sold
-
-
- )}
-
-
- {/* Thumbnail gallery */}
- {galleryImages.length > 0 && (
-
-
- {galleryImages.map((img, index) => (
-
- ))}
-
- )}
-
-
- {/* Details */}
-
-
{artwork.title}
-
- {artwork.year && (
-
{artwork.year}
- )}
-
- {/* Price */}
-
- {isSold ? (
-
Sold
- ) : artwork.price ? (
-
{artwork.currency === 'GBP' ? '£' : '$'}{artwork.price.toLocaleString()}
- ) : (
-
Price on request
- )}
-
-
- {/* Add to cart */}
- {isAvailable && !isSold && (
-
-
-
- )}
-
- {/* Enquire button for sold or POA items */}
- {(isSold || !artwork.price) && (
-
-
- Enquire About This Work
-
-
- )}
-
- {/* Details */}
-
- {artwork.medium && (
-
- Medium
- {artwork.medium}
-
- )}
- {artwork.dimensions && (
-
- Dimensions
- {artwork.dimensions}
-
- )}
- {artwork.series && typeof artwork.series === 'object' && (
-
- Series
-
- {artwork.series.name}
-
-
- )}
-
-
- {/* Description */}
- {artwork.description && (
-
-
- About This Work
-
-
-
- )}
-
- {/* Shipping info */}
-
-
-
-
-
Secure Shipping
-
- All artworks are carefully packaged and shipped with insurance.
-
-
-
-
-
-
-
-
- {/* Related works */}
- {relatedWorks.length > 0 && (
-
-
-
You May Also Like
-
- {relatedWorks.map((work) => (
-
-
- {work.image && (
-
- )}
-
-
-
{work.title}
- {work.price && (
-
{work.currency === 'GBP' ? '£' : '$'}{work.price.toLocaleString()}
- )}
-
-
- ))}
-
-
-
- )}
-
+
);
}
diff --git a/frontend/src/lib/directus.ts b/frontend/src/lib/directus.ts
index 79ee202..57ca08b 100644
--- a/frontend/src/lib/directus.ts
+++ b/frontend/src/lib/directus.ts
@@ -180,15 +180,23 @@ export interface DirectusFile {
}
// Create the Directus client (untyped to avoid complex SDK type inference)
-const directusUrl = process.env.NEXT_PUBLIC_DIRECTUS_URL || 'https://katheryn-cms.jeffemmett.com';
+// Use internal Docker URL for server-side API calls (bypasses Cloudflare Access)
+const publicUrl = process.env.NEXT_PUBLIC_DIRECTUS_URL || 'https://katheryn-cms.jeffemmett.com';
+const directusUrl = process.env.DIRECTUS_INTERNAL_URL || publicUrl;
const apiToken = process.env.DIRECTUS_API_TOKEN || '';
+// For asset URLs: use internal Docker URL to bypass Cloudflare Access,
+// and use NEXT_PUBLIC_ token so it's available in client components
+const assetBaseUrl = process.env.NEXT_PUBLIC_DIRECTUS_ASSET_URL || publicUrl;
+const assetToken = process.env.NEXT_PUBLIC_DIRECTUS_ASSET_TOKEN || apiToken;
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const directus = createDirectus(directusUrl)
.with(staticToken(apiToken))
.with(rest());
// Helper to get asset URL (includes auth token since Directus assets require authentication)
+// Uses internal Docker URL when available to bypass Cloudflare Access
export function getAssetUrl(fileId: string | DirectusFile | undefined, options?: { width?: number; height?: number; quality?: number; format?: 'webp' | 'jpg' | 'png' }): string {
if (!fileId) return '/placeholder.jpg';
@@ -196,7 +204,7 @@ export function getAssetUrl(fileId: string | DirectusFile | undefined, options?:
const params = new URLSearchParams();
// Auth token required for asset access
- if (apiToken) params.append('access_token', apiToken);
+ if (assetToken) params.append('access_token', assetToken);
if (options) {
if (options.width) params.append('width', options.width.toString());
@@ -205,7 +213,7 @@ export function getAssetUrl(fileId: string | DirectusFile | undefined, options?:
if (options.format) params.append('format', options.format);
}
- return `${directusUrl}/assets/${id}?${params.toString()}`;
+ return `${assetBaseUrl}/assets/${id}?${params.toString()}`;
}
// Map Directus artwork fields to frontend Artwork interface