@@ -88,20 +88,27 @@ export async function redirectRoutes(fastify: FastifyInstance) {
8888
8989 if ( templateSlug ) {
9090 // Template-based URL: verify both template and link match
91+ // Also fetch template settings and org settings for URL fallback chain
9192 query = `
92- SELECT l.* FROM links l
93+ SELECT l.*, t.settings AS template_settings, o.settings AS org_settings
94+ FROM links l
9395 LEFT JOIN link_templates t ON l.template_id = t.id
96+ LEFT JOIN organizations o ON l.organization_id = o.id
9497 WHERE l.short_code = $1 AND t.slug = $2
9598 AND l.is_active = true
9699 AND (l.expires_at IS NULL OR l.expires_at > NOW())
97100 ` ;
98101 params = [ shortCode , templateSlug ] ;
99102 } else {
100103 // Legacy URL: just lookup by short code
104+ // Also fetch template settings and org settings for URL fallback chain
101105 query = `
102- SELECT * FROM links
103- WHERE short_code = $1 AND is_active = true
104- AND (expires_at IS NULL OR expires_at > NOW())
106+ SELECT l.*, t.settings AS template_settings, o.settings AS org_settings
107+ FROM links l
108+ LEFT JOIN link_templates t ON l.template_id = t.id
109+ LEFT JOIN organizations o ON l.organization_id = o.id
110+ WHERE l.short_code = $1 AND l.is_active = true
111+ AND (l.expires_at IS NULL OR l.expires_at > NOW())
105112 ` ;
106113 params = [ shortCode ] ;
107114 }
@@ -240,6 +247,14 @@ export async function redirectRoutes(fastify: FastifyInstance) {
240247 await storeFingerprintForClick ( clickId , fingerprintData ) ;
241248
242249 // Determine redirect URL for event emission (using same logic as main redirect)
250+ // Use the same fallback chain: link → template → workspace
251+ const tplSettings = link . template_settings || { } ;
252+ const oSettings = link . org_settings || { } ;
253+ const oAppConfig = oSettings . appConfig || { } ;
254+ const iosStoreUrl = link . ios_app_store_url || tplSettings . defaultIosUrl || oAppConfig . iosAppStoreUrl || null ;
255+ const androidStoreUrl = link . android_app_store_url || tplSettings . defaultAndroidUrl || oAppConfig . androidAppStoreUrl || null ;
256+ const webFallback = link . web_fallback_url || tplSettings . defaultWebFallbackUrl || oAppConfig . webFallbackUrl || null ;
257+
243258 let redirectUrl = link . original_url ;
244259 let redirectReason = 'original_url' ;
245260
@@ -250,9 +265,12 @@ export async function redirectRoutes(fastify: FastifyInstance) {
250265 } else if ( link . app_scheme && link . deep_link_path ) {
251266 redirectUrl = `${ link . app_scheme } ://${ link . deep_link_path . replace ( / ^ \/ / , '' ) } ` ;
252267 redirectReason = 'app_scheme' ;
253- } else if ( link . ios_app_store_url ) {
254- redirectUrl = link . ios_app_store_url ;
268+ } else if ( iosStoreUrl ) {
269+ redirectUrl = iosStoreUrl ;
255270 redirectReason = 'ios_app_store_url' ;
271+ } else if ( webFallback ) {
272+ redirectUrl = webFallback ;
273+ redirectReason = 'web_fallback_url' ;
256274 }
257275 } else if ( deviceType === 'android' ) {
258276 if ( link . android_app_link ) {
@@ -261,16 +279,19 @@ export async function redirectRoutes(fastify: FastifyInstance) {
261279 } else if ( link . app_scheme && link . deep_link_path ) {
262280 redirectUrl = `${ link . app_scheme } ://${ link . deep_link_path . replace ( / ^ \/ / , '' ) } ` ;
263281 redirectReason = 'app_scheme' ;
264- } else if ( link . android_app_store_url ) {
265- redirectUrl = link . android_app_store_url ;
282+ } else if ( androidStoreUrl ) {
283+ redirectUrl = androidStoreUrl ;
266284 redirectReason = 'android_app_store_url' ;
285+ } else if ( webFallback ) {
286+ redirectUrl = webFallback ;
287+ redirectReason = 'web_fallback_url' ;
267288 }
268- } else if ( deviceType === 'web' && link . web_fallback_url ) {
269- redirectUrl = link . web_fallback_url ;
289+ } else if ( deviceType === 'web' && webFallback ) {
290+ redirectUrl = webFallback ;
270291 redirectReason = 'web_fallback_url' ;
271292 }
272293
273- const finalRedirectUrl = buildRedirectUrl ( redirectUrl , link . utm_parameters ) ;
294+ const finalRedirectUrl = buildRedirectUrl ( redirectUrl , link . utm_parameters ) || redirectUrl ;
274295
275296 // Emit click event for real-time streaming to WebSocket clients
276297 emitClickEvent ( {
@@ -342,57 +363,79 @@ export async function redirectRoutes(fastify: FastifyInstance) {
342363 } ) ;
343364
344365 // Determine redirect URL based on device with smart fallback chain
366+ // Fallback chain: link URLs → template default URLs → workspace settings URLs
345367 const userAgent = request . headers [ 'user-agent' ] || '' ;
346368 const device = detectDevice ( userAgent ) ;
347369
370+ // Extract fallback URLs from template settings and org settings
371+ const templateSettings = link . template_settings || { } ;
372+ const orgSettings = link . org_settings || { } ;
373+ const orgAppConfig = orgSettings . appConfig || { } ;
374+
375+ // Resolve platform URLs with fallback chain: link → template → workspace
376+ const iosUrl = link . ios_app_store_url || templateSettings . defaultIosUrl || orgAppConfig . iosAppStoreUrl || null ;
377+ const androidUrl = link . android_app_store_url || templateSettings . defaultAndroidUrl || orgAppConfig . androidAppStoreUrl || null ;
378+ const webFallbackUrl = link . web_fallback_url || templateSettings . defaultWebFallbackUrl || orgAppConfig . webFallbackUrl || null ;
379+
348380 let redirectUrl = link . original_url ;
349381 let useSchemeUrl = false ; // Track if we're using a URI scheme URL
350382
351383 if ( device === 'ios' ) {
352384 // iOS Priority:
353385 // 1. Universal Link (HTTPS URL with AASA file) - if app installed, opens app
354386 // 2. URI scheme (myapp://path) - fallback when Universal Links fail
355- // 3. App Store URL - for users who don't have the app
356- // 4. Original URL - ultimate fallback
387+ // 3. App Store URL (link → template → workspace) - for users who don't have the app
388+ // 4. Web fallback URL - browser-based fallback
389+ // 5. Original URL - ultimate fallback
357390
358391 if ( link . ios_universal_link ) {
359392 redirectUrl = link . ios_universal_link ;
360393 } else if ( link . app_scheme && link . deep_link_path ) {
361394 // Build URI scheme URL: myapp://product/123
362395 redirectUrl = `${ link . app_scheme } ://${ link . deep_link_path . replace ( / ^ \/ / , '' ) } ` ;
363396 useSchemeUrl = true ;
364- } else if ( link . ios_app_store_url ) {
365- redirectUrl = link . ios_app_store_url ;
397+ } else if ( iosUrl ) {
398+ redirectUrl = iosUrl ;
399+ } else if ( webFallbackUrl ) {
400+ redirectUrl = webFallbackUrl ;
366401 }
367402
368403 } else if ( device === 'android' ) {
369404 // Android Priority:
370405 // 1. App Link (HTTPS URL with Digital Asset Links) - if app installed, opens app
371406 // 2. URI scheme (myapp://path) - fallback when App Links fail
372- // 3. Play Store URL - for users who don't have the app
373- // 4. Original URL - ultimate fallback
407+ // 3. Play Store URL (link → template → workspace) - for users who don't have the app
408+ // 4. Web fallback URL - browser-based fallback
409+ // 5. Original URL - ultimate fallback
374410
375411 if ( link . android_app_link ) {
376412 redirectUrl = link . android_app_link ;
377413 } else if ( link . app_scheme && link . deep_link_path ) {
378414 // Build URI scheme URL: myapp://product/123
379415 redirectUrl = `${ link . app_scheme } ://${ link . deep_link_path . replace ( / ^ \/ / , '' ) } ` ;
380416 useSchemeUrl = true ;
381- } else if ( link . android_app_store_url ) {
382- redirectUrl = link . android_app_store_url ;
417+ } else if ( androidUrl ) {
418+ redirectUrl = androidUrl ;
419+ } else if ( webFallbackUrl ) {
420+ redirectUrl = webFallbackUrl ;
383421 }
384422
385423 } else if ( device === 'web' ) {
386424 // Web fallback
387- redirectUrl = link . web_fallback_url || link . original_url ;
425+ redirectUrl = webFallbackUrl || link . original_url ;
426+ }
427+
428+ // If no URL found at all, return a user-friendly error
429+ if ( ! redirectUrl ) {
430+ return reply . status ( 404 ) . send ( { error : 'No destination URL configured for this link' } ) ;
388431 }
389432
390433 // Build final URL with parameters
391434 let finalUrl = redirectUrl ;
392435
393436 if ( ! useSchemeUrl ) {
394437 // For HTTP(S) URLs, add UTM parameters
395- finalUrl = buildRedirectUrl ( redirectUrl , link . utm_parameters ) ;
438+ finalUrl = buildRedirectUrl ( redirectUrl , link . utm_parameters ) || redirectUrl ;
396439
397440 // Add deep link parameters as query params
398441 if ( link . deep_link_parameters && Object . keys ( link . deep_link_parameters ) . length > 0 ) {
@@ -435,7 +478,7 @@ export async function redirectRoutes(fastify: FastifyInstance) {
435478 fullSchemeUrl += ( fullSchemeUrl . includes ( '?' ) ? '&' : '?' ) + params . toString ( ) ;
436479 }
437480
438- const storeFallback = link . ios_app_store_url || link . web_fallback_url || link . original_url ;
481+ const storeFallback = iosUrl || webFallbackUrl || link . original_url ;
439482 return reply
440483 . header ( 'Content-Type' , 'text/html; charset=utf-8' )
441484 . send ( generateInterstitialHTML ( fullSchemeUrl , storeFallback , link . title || link . og_title ) ) ;
0 commit comments