Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions Plugin/Framework/App/Response/Http.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php declare(strict_types=1);

namespace MageOS\ThemeOptimization\Plugin\Framework\App\Response;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\PageCache\Model\Config;
use Magento\Store\Model\ScopeInterface;

/**
* Plugin to modify cache headers for BFCache functionality
*/
class Http
{
/** @var string */
public const XML_PATH_ENABLE = 'system/bfcache/general/enable';

/** @var string */
public const XML_PATH_EXCLUDE_URL_PATTERNS = 'system/bfcache/scope/exclude_url_patterns';

/** @var bool */
private $isRequestCacheable = false;

/**
* @param Config $config
* @param ScopeConfigInterface $scopeConfig
* @param HttpRequest $request
*/
public function __construct(
private Config $config,
private ScopeConfigInterface $scopeConfig,
private HttpRequest $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->isRequestInExcludePatterns($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 the request URI contains any excluded URL patterns (case-insensitive, partial match).
*
* @param string $requestURI
* @return bool
*/
private function isRequestInExcludePatterns(string $requestURI): bool
{
$patterns = $this->getConfig(self::XML_PATH_EXCLUDE_URL_PATTERNS);

if (empty($patterns)) {
return false;
}

foreach ($this->parseExcludePatterns($patterns) as $pattern) {
if ($pattern !== '' && mb_stripos($requestURI, $pattern, 0, 'UTF-8') !== false) {
return true;
}
}

return false;
}

/**
* Parse exclude patterns from config string.
*
* @param string $patterns
* @return array
*/
private function parseExcludePatterns(string $patterns): array
{
return array_filter(array_map('trim', explode("\n", $patterns)));
}

/**
* 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
*/
private function getConfig(string $configPath, $store = null): string
{
return (string)$this->scopeConfig->getValue(
$configPath,
ScopeInterface::SCOPE_STORE,
$store
);
}
}
71 changes: 71 additions & 0 deletions ViewModel/BfCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types=1);

namespace MageOS\ThemeOptimization\ViewModel;

use Magento\Customer\Model\Context as CustomerContext;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Http\Context;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Store\Model\ScopeInterface;

/**
* BFCache ViewModel
* Provides data and configuration for BFCache templates.
* Handles configuration management and business logic for frontend templates.
*/
class BfCache implements ArgumentInterface
{
/** @var string */
private const XML_PATH_ENABLE_USER_INTERACTION_RELOAD_MINICART =
'system/bfcache/general/enable_user_interaction_reload_minicart';

/** @var string */
private const XML_PATH_AUTO_CLOSE_MENU_MOBILE =
'system/bfcache/general/auto_close_menu_mobile';

/**
* @param ScopeConfigInterface $scopeConfig
* @param Context $httpContext
*/
public function __construct(
private ScopeConfigInterface $scopeConfig,
private Context $httpContext
) {
}

/**
* Check if mini cart should reload on user interaction
*
* @return bool
*/
public function isReloadMiniCartOnInteraction(): 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);
}
}
56 changes: 56 additions & 0 deletions etc/adminhtml/system.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,61 @@
</field>
</group>
</section>
<section id="system">
<group id="bfcache" translate="label" sortOrder="590" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Back/Forward Cache</label>
<comment><![CDATA[
<p>Go back and forwards in the browser history on most pages instantly, by allowing them to be cached in the browser temporarily.</p>
]]></comment>
<group id="general" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>General Settings</label>
<attribute type="expanded">1</attribute>
<field id="enable" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>Enable Back/Forward Cache</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<comment><![CDATA[
Enable browser back/forward cache to store pages in memory for faster navigation.
]]>
</comment>
</field>
<field id="enable_user_interaction_reload_minicart" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>Update Mini Cart on User Interaction</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<comment><![CDATA[
<b>Yes:</b> Mini cart updates only after user interaction when page is restored from cache.<br>
<b>No:</b> Mini cart updates immediately on page restore.<br>
<b>Recommended "Yes" to maintain optimal Page Speed and Core Web Vitals scores.</b>
]]>
</comment>
</field>
<field id="auto_close_menu_mobile" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>Auto Close Menu</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<comment><![CDATA[
Automatically close open menus when page is restored from back/forward cache (for compatible themes).
]]>
</comment>
</field>
</group>
<group id="scope" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Cache Exclusions</label>
<comment><![CDATA[
<p><b>Note:</b> This configuration is optional for most sites.<br>
The extension automatically excludes non-cacheable URLs from back/forward cache.</p>
<p>Use the blacklist below if you have custom cached URLs that load private data via JavaScript.</p>
]]></comment>
<field id="exclude_url_patterns" translate="label" type="textarea" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>Exclude URLs</label>
<comment><![CDATA[
Enter URL parts (substring), one per line. Any URL containing one of these patterns will be excluded from the back/forward cache.<br>
Example:<br>
<code>customer</code><br>
<code>checkout</code><br>
<code>account</code>
]]></comment>
</field>
</group>
</group>
</section>
</system>
</config>
9 changes: 9 additions & 0 deletions etc/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,14 @@ productalert</exclude_paths>
<exclude_selectors>.do-not-prerender</exclude_selectors>
</speculation_rules>
</dev>
<system>
<bfcache>
<general>
<enable>1</enable>
<enable_user_interaction_reload_minicart>1</enable_user_interaction_reload_minicart>
<auto_close_menu_mobile>1</auto_close_menu_mobile>
</general>
</bfcache>
</system>
</default>
</config>
7 changes: 7 additions & 0 deletions etc/frontend/di.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\App\Response\Http">
<plugin name="theme_optimization_bfcache_response_header_plugin" type="MageOS\ThemeOptimization\Plugin\Framework\App\Response\Http"/>
</type>
</config>
2 changes: 2 additions & 0 deletions etc/module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<sequence>
<module name="Magento_Framework" />
<module name="Magento_Store" />
<module name="Magento_PageCache" />
<module name="Magento_Customer" />
</sequence>
</module>
</config>
7 changes: 7 additions & 0 deletions view/frontend/layout/default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
<argument name="view_model" xsi:type="object">\MageOS\ThemeOptimization\ViewModel\SpeculationRules</argument>
</arguments>
</block>
<block name="theme-optimization.bfcache.main"
template="MageOS_ThemeOptimization::bfcache/handler.phtml"
ifconfig="system/bfcache/general/enable">
<arguments>
<argument name="bfcache_config" xsi:type="object">\MageOS\ThemeOptimization\ViewModel\BfCache</argument>
</arguments>
</block>
</referenceContainer>
</body>
</page>
14 changes: 14 additions & 0 deletions view/frontend/layout/default_hyva.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="before.body.end">
<block name="theme-optimization.bfcache.main"
template="MageOS_ThemeOptimization::hyva/bfcache/handler.phtml"
ifconfig="system/bfcache/general/enable">
<arguments>
<argument name="bfcache_config" xsi:type="object">\MageOS\ThemeOptimization\ViewModel\BfCache</argument>
</arguments>
</block>
</referenceBlock>
</body>
</page>
Loading