fix: FluxAI image paths now include the node slug
Deploy to VPS / deploy (push) Has been cancelled

The two AI tool cards rendered inside the chat — CaseStudyViewer (the
"Show me proven installations" card) and EquipmentConfigurator (the
"Show equipment specs" card) — were composing image URLs without the
node-slug segment:

  /cases/<filename>            ← what the cards were emitting (404)
  /cases/<nodeSlug>/<filename> ← what the public site actually serves

Result: cover images and gallery thumbnails inside chat cards came back
as broken-image icons, while the same files rendered fine on
/en/applications/<slug> and inside the CaseStudyModal (those
already used the correct path).

Fix: both components now derive the slug from data.title with the same
nodeToSlug() the public pages use, and prefix it on every /cases/
URL — cover and gallery thumbnails alike.

CHANGES (2 files)
- src/components/ai/CaseStudyViewer.tsx
  - Added local nodeToSlug() helper (mirrors ApplicationClient + assetFolders)
  - coverSrc:        /cases/${nodeSlug}/${mediaFileName}
  - gallery image:   /cases/${nodeSlug}/${img}

- src/components/ai/EquipmentConfigurator.tsx
  - Added local nodeToSlug() helper
  - coverSrc:        /cases/${nodeSlug}/${mediaFileName}

No backend / API / DB changes. Pure client-side path correction.
This commit is contained in:
2026-05-05 09:25:45 -05:00
parent 014a9eb094
commit fece168486
2 changed files with 18 additions and 3 deletions
+10 -2
View File
@@ -5,6 +5,13 @@ import { MapPin, Factory, Zap, Clock, ChevronDown, ArrowRight, Globe2, Image as
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
// Slugify the node title the same way ApplicationClient + assetFolders do —
// keeps image paths consistent with the on-disk layout
// (/cases/<nodeSlug>/<file>).
function nodeToSlug(title: string): string {
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
}
// ── Interface matches GlobalNode shape from Prisma ── // ── Interface matches GlobalNode shape from Prisma ──
interface CaseStudyData { interface CaseStudyData {
found: boolean; found: boolean;
@@ -50,7 +57,8 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
if (!data.found) return null; if (!data.found) return null;
const accent = ACCENTS[data.industry] || ACCENTS.textile; const accent = ACCENTS[data.industry] || ACCENTS.textile;
const coverSrc = data.mediaFileName ? `/cases/${data.mediaFileName}` : null; const nodeSlug = nodeToSlug(data.title);
const coverSrc = data.mediaFileName ? `/cases/${nodeSlug}/${data.mediaFileName}` : null;
const appLabel = data.application.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()); const appLabel = data.application.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase());
return ( return (
@@ -192,7 +200,7 @@ export default function CaseStudyViewer({ data }: { data: CaseStudyData }) {
<div className="grid grid-cols-3 gap-1.5 rounded-lg overflow-hidden"> <div className="grid grid-cols-3 gap-1.5 rounded-lg overflow-hidden">
{data.gallery.slice(0, 6).map((img, i) => ( {data.gallery.slice(0, 6).map((img, i) => (
<div key={i} className="relative aspect-square bg-[#F5F5F7] dark:bg-[#1D1D1F] rounded-md overflow-hidden"> <div key={i} className="relative aspect-square bg-[#F5F5F7] dark:bg-[#1D1D1F] rounded-md overflow-hidden">
<Image src={`/cases/${img}`} alt={`Gallery ${i + 1}`} fill className="object-cover" sizes="120px" /> <Image src={`/cases/${nodeSlug}/${img}`} alt={`Gallery ${i + 1}`} fill className="object-cover" sizes="120px" />
</div> </div>
))} ))}
</div> </div>
+8 -1
View File
@@ -5,6 +5,12 @@ import { Cpu, ArrowRight, Settings2, ChevronDown, MapPin, Factory, Zap, Box } fr
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
// Same slugger as the public-facing pages so cover images resolve to the
// /cases/<nodeSlug>/<file> layout that's actually on disk.
function nodeToSlug(title: string): string {
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
}
// ── Interface matches GlobalNode + specificDatasheetJson from Prisma ── // ── Interface matches GlobalNode + specificDatasheetJson from Prisma ──
interface EquipmentData { interface EquipmentData {
found: boolean; found: boolean;
@@ -39,7 +45,8 @@ export default function EquipmentConfigurator({ data }: { data: EquipmentData })
if (!data.found) return null; if (!data.found) return null;
const coverSrc = data.mediaFileName ? `/cases/${data.mediaFileName}` : null; const nodeSlug = nodeToSlug(data.title);
const coverSrc = data.mediaFileName ? `/cases/${nodeSlug}/${data.mediaFileName}` : null;
const appLabel = data.application.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()); const appLabel = data.application.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase());
// Find key specs for header pills (power, frequency, model — from datasheet) // Find key specs for header pills (power, frequency, model — from datasheet)