From b99e5a83a8164d089993a8236e0beedbf84c7e77 Mon Sep 17 00:00:00 2001 From: bfintal Date: Wed, 29 Jul 2026 12:31:33 +0800 Subject: [PATCH] fix: prevent Design Library from showing blank on fetch errors Guard against undefined library responses and stop caching failed CDN fetches so the Design Library can load designs or surface errors instead of crashing. Co-authored-by: Cursor --- src/design-library/index.js | 56 ++++++++++++++------- src/design-library/init.php | 29 ++++++++--- src/lazy-components/design-library/index.js | 5 ++ 3 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/design-library/index.js b/src/design-library/index.js index 72c87d36c..ea3af50e8 100644 --- a/src/design-library/index.js +++ b/src/design-library/index.js @@ -5,37 +5,46 @@ const LATEST_API_VERSION = 'v4' let designLibrary = {} let designs = {} -let pages = {} export const getBlockName = block => block.replace( /^[\w-]+\//, '' ) +const hasLibraryError = library => + library && ( library.wp_remote_get_error || library.content_error ) + export const fetchDesignLibrary = async ( forceReset = false, version = '', type = 'patterns' ) => { if ( forceReset ) { doAction( 'stackable.design-library.reset-cache' ) designLibrary = {} designs = {} - pages = {} } - if ( ( type === 'patterns' && ! Object.keys( designs ).length ) || - ( type === 'pages' && ! Object.keys( pages ).length ) - ) { + const needsFetch = ( type === 'patterns' || type === 'pages' ) && ! designLibrary[ type ] + + if ( needsFetch ) { const results = await apiFetch( { path: `/stackable/v2/design_library/${ type }${ forceReset ? '/reset' : '' }`, method: 'GET', - } ) - const designsPerType = results + } ) || {} - designLibrary[ type ] = designsPerType + designLibrary[ type ] = results - if ( type === 'patterns' ) { - designs = designsPerType[ LATEST_API_VERSION ] ?? {} - } else { - pages = designsPerType[ LATEST_API_VERSION ] ?? {} + if ( hasLibraryError( results ) ) { + if ( type === 'patterns' ) { + designs = {} + } + } else if ( type === 'patterns' ) { + designs = results[ LATEST_API_VERSION ] ?? {} } } - return designLibrary[ type ]?.[ version || LATEST_API_VERSION ] ?? designLibrary[ type ] + const library = designLibrary[ type ] || {} + + // Return the raw response when it contains fetch/parse errors so callers can handle them. + if ( hasLibraryError( library ) ) { + return library + } + + return library[ version || LATEST_API_VERSION ] ?? library } export const fetchDesign = async designId => { @@ -61,10 +70,17 @@ export const getDesigns = async ( { reset = false, type = 'patterns', } ) => { - const designLibrary = await fetchDesignLibrary( reset, LATEST_API_VERSION, type ) + const library = await fetchDesignLibrary( reset, LATEST_API_VERSION, type ) + + if ( ! library || typeof library !== 'object' ) { + const error = { message: 'Failed to load design library.' } + // eslint-disable-next-line no-console + console.error( 'Stackable: ', error ) + return { error } + } - if ( designLibrary.wp_remote_get_error || designLibrary.content_error ) { - const error = designLibrary.wp_remote_get_error ?? designLibrary.content_error + if ( hasLibraryError( library ) ) { + const error = library.wp_remote_get_error ?? library.content_error // eslint-disable-next-line no-console console.error( 'Stackable: ', error ) return { error } @@ -75,7 +91,7 @@ export const getDesigns = async ( { await fetchDesignLibrary() } - return Object.values( designLibrary ) + return Object.values( library ) } export const filterDesigns = async ( { @@ -104,12 +120,16 @@ export const filterDesigns = async ( { export const getDesign = async ( designId, version = '' ) => { const library = await fetchDesignLibrary( false, version ) + if ( ! library || hasLibraryError( library ) ) { + return null + } + const meta = library[ designId ] let design = await applyFilters( 'stackable.design-library.get-design', null, designId, meta, version ) // Every design has their own template file which contains the entire design, get that. - if ( ! design && meta.template ) { + if ( ! design && meta?.template ) { design = await fetchDesign( designId, version ) } diff --git a/src/design-library/init.php b/src/design-library/init.php index e48bb1a6d..ee198a9fa 100644 --- a/src/design-library/init.php +++ b/src/design-library/init.php @@ -240,6 +240,19 @@ public function get_design_library_from_cloud( $type = 'patterns' ) { $designs = get_transient( $transient_name ); + // Retry when a previous failed fetch was cached without usable library data. + if ( + ! empty( $designs ) && + ( + isset( $designs['wp_remote_get_error'] ) || + isset( $designs['content_error'] ) + ) && + empty( $designs[ self::API_VERSION ] ) + ) { + delete_transient( $transient_name ); + $designs = false; + } + // Fetch designs. if ( empty( $designs ) ) { $designs = array(); @@ -253,6 +266,8 @@ public function get_design_library_from_cloud( $type = 'patterns' ) { 'code' => $response->get_error_code(), 'message' => $response->get_error_message(), ); + $designs[ self::API_VERSION ] = null; + // Do not cache failed fetches so the next request can retry. } else { $content_body = wp_remote_retrieve_body( $response ); $content = apply_filters( 'stackable_design_library_retreive_body', $content_body ); @@ -263,14 +278,16 @@ public function get_design_library_from_cloud( $type = 'patterns' ) { $designs['content_error'] = array( 'message' => $content_body, ); + $designs[ self::API_VERSION ] = null; + // Do not cache failed fetches so the next request can retry. + } else { + // We add the latest designs in the `v4` area. + $designs[ self::API_VERSION ] = $content; + + // Cache successful results. + set_transient( $transient_name, $designs, 7 * DAY_IN_SECONDS ); } } - - // We add the latest designs in the `v4` area. - $designs[ self::API_VERSION ] = $content; - - // Cache results. - set_transient( $transient_name, $designs, 7 * DAY_IN_SECONDS ); } if ( $type === 'pages' ) { diff --git a/src/lazy-components/design-library/index.js b/src/lazy-components/design-library/index.js index 4a53a20b8..b0ee9b545 100644 --- a/src/lazy-components/design-library/index.js +++ b/src/lazy-components/design-library/index.js @@ -102,6 +102,11 @@ const ModalDesignLibrary = props => { setSidebarDesigns( _designs ) setSelectedCategory( '' ) + } ).catch( error => { + setSidebarDesigns( [] ) + setErrors( { + message: error?.message || String( error ), + } ) } ).finally( () => { setDoReset( false ) setIsBusy( false )