From a7369e2669c0cc02442b4540503403f58e4f7e30 Mon Sep 17 00:00:00 2001 From: JaJuMa Date: Fri, 5 Sep 2025 15:42:07 +0700 Subject: [PATCH 1/5] Implement Back/Forward Cache functionality with configuration options and view models --- Plugin/Framework/App/Response/Http.php | 172 +++++++++++++++++ ViewModel/BfCache.php | 76 ++++++++ etc/adminhtml/system.xml | 52 ++++++ etc/frontend/di.xml | 7 + etc/module.xml | 1 + view/frontend/layout/default.xml | 7 + view/frontend/layout/default_hyva.xml | 14 ++ view/frontend/templates/bfcache/handler.phtml | 162 ++++++++++++++++ .../templates/hyva/bfcache/handler.phtml | 173 ++++++++++++++++++ 9 files changed, 664 insertions(+) create mode 100644 Plugin/Framework/App/Response/Http.php create mode 100644 ViewModel/BfCache.php create mode 100644 etc/frontend/di.xml create mode 100644 view/frontend/layout/default_hyva.xml create mode 100644 view/frontend/templates/bfcache/handler.phtml create mode 100644 view/frontend/templates/hyva/bfcache/handler.phtml diff --git a/Plugin/Framework/App/Response/Http.php b/Plugin/Framework/App/Response/Http.php new file mode 100644 index 0000000..e6fce11 --- /dev/null +++ b/Plugin/Framework/App/Response/Http.php @@ -0,0 +1,172 @@ +config = $config; + $this->scopeConfig = $scopeConfig; + $this->request = $request; + } + + /** + * Intercept before setting no-cache headers to determine if request is cacheable + * + * @param \Magento\Framework\App\Response\Http $subject + * @return void + */ + public function beforeSetNoCacheHeaders(\Magento\Framework\App\Response\Http $subject): void + { + if ($this->config->getType() !== Config::BUILT_IN || !$this->isEnabled()) { + return; + } + + $cacheControlHeader = $subject->getHeader('Cache-Control'); + if (!$cacheControlHeader) { + return; + } + + $cacheControl = $cacheControlHeader->getFieldValue(); + $requestURI = ltrim($this->request->getRequestURI(), '/'); + + if ($this->isRequestCacheable($cacheControl) && !$this->isRequestInBlackListUrls($requestURI)) { + $this->isRequestCacheable = true; + } + } + + /** + * Update cache headers after setting no-cache headers + * + * @param \Magento\Framework\App\Response\Http $subject + * @param mixed $result + * @return mixed + */ + public function afterSetNoCacheHeaders(\Magento\Framework\App\Response\Http $subject, $result) + { + if ($this->config->getType() !== Config::BUILT_IN || !$this->isEnabled()) { + return $result; + } + + $cacheControlHeader = $subject->getHeader('Cache-Control'); + if (!$cacheControlHeader) { + return $result; + } + + if ($this->isRequestCacheable == true) { + $cacheControlHeader = $subject->getHeader('cache-control'); + $cacheControlHeader->removeDirective('no-store'); + } + $this->isRequestCacheable = false; + + return $result; + } + + /** + * Check if request is cacheable based on cache control header + * + * @param string $cacheControl + * @return bool + */ + private function isRequestCacheable(string $cacheControl): bool + { + return (bool) preg_match('/public.*s-maxage=(\d+)/', $cacheControl); + } + + /** + * Check if request URI matches blacklisted URLs + * + * @param string $requestURI + * @return bool + */ + private function isRequestInBlackListUrls(string $requestURI): bool + { + $blackListUrls = $this->convertListUrls(self::XML_PATH_BLACK_LIST_URLS); + if (!$blackListUrls) { + return false; + } + + return (bool) preg_match('/' . $blackListUrls . '/', $requestURI); + } + + /** + * Check if BFCache is enabled + * + * @return bool + */ + private function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ENABLE, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Get configuration value by path + * + * @param string $configPath + * @param int|string|null $store + * @return string|null + */ + private function getConfig(string $configPath, $store = null): ?string + { + return $this->scopeConfig->getValue( + $configPath, + ScopeInterface::SCOPE_STORE, + $store + ); + } + + /** + * Convert comma-separated URLs to pipe-separated regex pattern + * + * @param string $configPath + * @return string + */ + private function convertListUrls(string $configPath): string + { + $listUrls = $this->getConfig($configPath); + if (!$listUrls) { + return ''; + } + + $urlList = array_map('trim', explode(',', $listUrls)); + $urlList = array_filter($urlList); + + return implode('|', array_map('preg_quote', $urlList, array_fill(0, count($urlList), '/'))); + } +} diff --git a/ViewModel/BfCache.php b/ViewModel/BfCache.php new file mode 100644 index 0000000..509cd98 --- /dev/null +++ b/ViewModel/BfCache.php @@ -0,0 +1,76 @@ +scopeConfig = $scopeConfig; + $this->httpContext = $httpContext; + } + + /** + * Check if user interaction should refresh minicart + * + * @return bool + */ + public function getEnableUserInteractionRefreshMiniCart(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_ENABLE_USER_INTERACTION_RELOAD_MINICART, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Check if mobile menu should auto-close + * + * @return bool + */ + public function autoCloseMenuMobile(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_AUTO_CLOSE_MENU_MOBILE, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Check if customer is logged in + * + * @return bool + */ + public function isCustomerLoggedIn(): bool + { + return (bool) $this->httpContext->getValue(CustomerContext::CONTEXT_AUTH); + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 974bf87..c6cdee9 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -51,5 +51,57 @@ +
+ + + Configure browser's back/forward cache to improve navigation performance by storing pages in memory.

+ ]]>
+ + + 1 + + + Magento\Config\Model\Config\Source\Yesno + + + + + + Magento\Config\Model\Config\Source\Yesno + Yes: Mini cart updates only after user interaction when page is restored from cache.
+ No: Mini cart updates immediately on page restore.
+ Recommended "Yes" to maintain optimal Page Speed and Core Web Vitals scores. + ]]> +
+
+ + + Magento\Config\Model\Config\Source\Yesno + + + +
+ + + Note: This configuration is optional for most sites.
+ The extension automatically excludes non-cacheable URLs from back/forward cache.

+

Use the blacklist below if you have custom cached URLs that load private data via JavaScript.

+ ]]>
+ + + + +
+
+
diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml new file mode 100644 index 0000000..b86d0f1 --- /dev/null +++ b/etc/frontend/di.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/etc/module.xml b/etc/module.xml index 2dd2d99..f74465d 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -4,6 +4,7 @@ + diff --git a/view/frontend/layout/default.xml b/view/frontend/layout/default.xml index 56a29d7..a494de5 100644 --- a/view/frontend/layout/default.xml +++ b/view/frontend/layout/default.xml @@ -10,6 +10,13 @@ \MageOS\ThemeOptimization\ViewModel\SpeculationRules + + + \MageOS\ThemeOptimization\ViewModel\BfCache + + diff --git a/view/frontend/layout/default_hyva.xml b/view/frontend/layout/default_hyva.xml new file mode 100644 index 0000000..1467ee0 --- /dev/null +++ b/view/frontend/layout/default_hyva.xml @@ -0,0 +1,14 @@ + + + + + + + \MageOS\ThemeOptimization\ViewModel\BfCache + + + + + diff --git a/view/frontend/templates/bfcache/handler.phtml b/view/frontend/templates/bfcache/handler.phtml new file mode 100644 index 0000000..1c97fce --- /dev/null +++ b/view/frontend/templates/bfcache/handler.phtml @@ -0,0 +1,162 @@ +getData('bfcache_config'); +?> + +isCustomerLoggedIn() ? 'true' : 'false'; +$enableUserInteractionRefresh = $bfcacheConfig->getEnableUserInteractionRefreshMiniCart() ? 'true' : 'false'; +$autoCloseMenuMobile = $bfcacheConfig->autoCloseMenuMobile() ? 'true' : 'false'; + +$script = <<