@@ -23,6 +23,8 @@ import { resolveThemeConfigImageUrls } from "../../../utils/theme-config-images.
2323import { slugSchema } from "../../../schemas/_shared/primitives.js" ;
2424import { createTRPCRouter , publicProcedure } from "../../init.js" ;
2525
26+ const PRODUCTS_BUCKET = "products" ;
27+
2628/**
2729 * UPID schema: 16-character alphanumeric identifier
2830 */
@@ -38,6 +40,70 @@ const getThemePreviewSchema = z.object({
3840 brandSlug : slugSchema ,
3941} ) ;
4042
43+ /**
44+ * Escape regex metacharacters for a dynamic path pattern.
45+ */
46+ function escapeRegExp ( value : string ) : string {
47+ return value . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
48+ }
49+
50+ /**
51+ * Decode URI path segments safely.
52+ */
53+ function decodeStoragePath ( path : string ) : string {
54+ return path
55+ . split ( "/" )
56+ . map ( ( segment ) => {
57+ try {
58+ return decodeURIComponent ( segment ) ;
59+ } catch {
60+ return segment ;
61+ }
62+ } )
63+ . join ( "/" ) ;
64+ }
65+
66+ /**
67+ * Extract a bucket object path from known Supabase storage URL shapes.
68+ */
69+ function extractStorageObjectPath (
70+ value : string ,
71+ bucket : string ,
72+ ) : string | null {
73+ const escapedBucket = escapeRegExp ( bucket ) ;
74+ const pattern = new RegExp (
75+ `(?:https?:\\/\\/[^/]+)?\\/storage\\/v1\\/object\\/(?:public|sign)\\/${ escapedBucket } \\/(.+?)(?:[?#].*)?$` ,
76+ "i" ,
77+ ) ;
78+ const match = value . match ( pattern ) ;
79+ if ( ! match ?. [ 1 ] ) return null ;
80+ return decodeStoragePath ( match [ 1 ] ) ;
81+ }
82+
83+ /**
84+ * Resolve snapshot image values to a current public URL on the configured storage domain.
85+ */
86+ function resolveSnapshotProductImageUrl (
87+ storageClient : Parameters < typeof getPublicUrl > [ 0 ] ,
88+ imageValue : string | null | undefined ,
89+ ) : string | null {
90+ if ( ! imageValue ) return null ;
91+
92+ const normalizedImage =
93+ extractStorageObjectPath ( imageValue , PRODUCTS_BUCKET ) ?? imageValue ;
94+ if (
95+ normalizedImage . startsWith ( "http://" ) ||
96+ normalizedImage . startsWith ( "https://" )
97+ ) {
98+ return normalizedImage ;
99+ }
100+
101+ return (
102+ getPublicUrl ( storageClient , PRODUCTS_BUCKET , normalizedImage ) ??
103+ normalizedImage
104+ ) ;
105+ }
106+
41107export const dppPublicRouter = createTRPCRouter ( {
42108 /**
43109 * Fetch theme data for screenshot preview.
@@ -115,24 +181,11 @@ export const dppPublicRouter = createTRPCRouter({
115181 ? getPublicUrl ( ctx . supabase , "dpp-themes" , result . theme . stylesheetPath )
116182 : null ;
117183
118- // Resolve product image in the snapshot to public URL
119- let productImageUrl : string | null = null ;
120- const snapshotImage = result . snapshot . productAttributes ?. image ;
121- if ( snapshotImage && typeof snapshotImage === "string" ) {
122- // Check if it's already a full URL or a storage path
123- if (
124- snapshotImage . startsWith ( "http://" ) ||
125- snapshotImage . startsWith ( "https://" )
126- ) {
127- productImageUrl = snapshotImage ;
128- } else {
129- productImageUrl = getPublicUrl (
130- ctx . supabase ,
131- "products" ,
132- snapshotImage ,
133- ) ;
134- }
135- }
184+ // Resolve product image in the snapshot to a current public URL.
185+ const productImageUrl = resolveSnapshotProductImageUrl (
186+ ctx . supabase ,
187+ result . snapshot . productAttributes ?. image ,
188+ ) ;
136189
137190 // Resolve image paths in themeConfig to full URLs
138191 const resolvedThemeConfig = resolveThemeConfigImageUrls (
@@ -266,23 +319,11 @@ export const dppPublicRouter = createTRPCRouter({
266319 ? getPublicUrl ( ctx . supabase , "dpp-themes" , result . theme . stylesheetPath )
267320 : null ;
268321
269- // Resolve product image in the snapshot to public URL
270- let productImageUrl : string | null = null ;
271- const snapshotImage = result . snapshot . productAttributes ?. image ;
272- if ( snapshotImage && typeof snapshotImage === "string" ) {
273- if (
274- snapshotImage . startsWith ( "http://" ) ||
275- snapshotImage . startsWith ( "https://" )
276- ) {
277- productImageUrl = snapshotImage ;
278- } else {
279- productImageUrl = getPublicUrl (
280- ctx . supabase ,
281- "products" ,
282- snapshotImage ,
283- ) ;
284- }
285- }
322+ // Resolve product image in the snapshot to a current public URL.
323+ const productImageUrl = resolveSnapshotProductImageUrl (
324+ ctx . supabase ,
325+ result . snapshot . productAttributes ?. image ,
326+ ) ;
286327
287328 // Resolve image paths in themeConfig to full URLs
288329 const resolvedThemeConfig = resolveThemeConfigImageUrls (
0 commit comments