<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>gj</title>
	<atom:link href="https://blog.gaiterjones.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.gaiterjones.com/</link>
	<description>gaiterjones</description>
	<lastBuildDate>Fri, 25 Apr 2025 09:49:59 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8</generator>
	<item>
		<title>Are Blogs Dead?</title>
		<link>https://blog.gaiterjones.com/are-blogs-dead/</link>
					<comments>https://blog.gaiterjones.com/are-blogs-dead/#respond</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Fri, 25 Apr 2025 09:39:47 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2481</guid>

					<description><![CDATA[Are Blogs Dead? It’s a question I’ve been asking myself. I started this blog way back in 2010. For years, it was a place where I shared thoughts, ideas, and...<a class="more-link" href="https://blog.gaiterjones.com/are-blogs-dead/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<h1>Are Blogs Dead?</h1>
<p>It’s a question I’ve been asking myself.</p>
<p>I started this blog way back in 2010.<br />
For years, it was a place where I shared thoughts, ideas, and whatever else was on my mind.<br />
But then it kinda got left behind, life happened, things changed.<br />
And like many others, I just stopped posting here.</p>
<p>At first, I blamed it on being too busy.<br />
Then on not having anything to say.<br />
But if I&#8217;m honest, the real reason has become clearer lately:</p>
<p><strong>The internet has changed.</strong></p>
<p>In 2025, it feels like blogs — once vibrant, personal spaces — have been pushed to the sidelines.<br />
Google has changed too.<br />
It’s not the same search engine we use to know, now, it feels like AI is answering everything before you even have a chance to click a link.</p>
<p>People don’t &#8220;browse&#8221; the web like they used to.<br />
They ask AI a question and get an answer immediately.<br />
Quick.<br />
Effortless.<br />
Efficient.</p>
<p>Who needs to scroll through endless blog posts anymore?</p>
<p><strong>Right&#8230;?</strong></p>
<p>But here&#8217;s the thing.<br />
Last night, I realised something.<br />
Maybe blogs aren’t dead.<br />
Maybe they’ve just evolved — like everything else online.</p>
<p>A blog is still a place where you own your words.<br />
Where you <em>choose</em> the conversation.<br />
Where you&#8217;re not boxed in by an algorithm, a feed, or a chatbot&#8217;s summarised answer.</p>
<p>AI can give you an answer.<br />
But a blog gives you <em>a person</em>.<br />
A real voice, not just a response.</p>
<p>So maybe it&#8217;s not that blogs are dead.<br />
Maybe we just forgot why they mattered in the first place.</p>
<p>Maybe it&#8217;s time to start posting again&#8230;</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/are-blogs-dead/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PHP Passwordless User Login with WEB3 Connect and Metamask</title>
		<link>https://blog.gaiterjones.com/php-passwordless-user-login-with-web3-connect-and-metamask/</link>
					<comments>https://blog.gaiterjones.com/php-passwordless-user-login-with-web3-connect-and-metamask/#respond</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Thu, 23 Dec 2021 15:40:31 +0000</pubDate>
				<category><![CDATA[WEB3]]></category>
		<category><![CDATA[connect]]></category>
		<category><![CDATA[passwordless authentication web3 connect passwordless authentication]]></category>
		<category><![CDATA[web3]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2446</guid>

					<description><![CDATA[WEB3 is becoming the buzzword of the year &#8211; if you don&#8217;t know exactly what it is join the club! Suffice to say that WEB3 will decentralise the Internet enabling...<a class="more-link" href="https://blog.gaiterjones.com/php-passwordless-user-login-with-web3-connect-and-metamask/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<p>WEB3 is becoming the buzzword of the year &#8211; if you don&#8217;t know exactly what it is join the club! Suffice to say that WEB3 will <em>decentralise</em> the Internet enabling us to do all the stuff we love on the Internet with a whole load of new decentralised blockchain applications (dapps).</p>
<p>If you have already used any dapps you will be familiar with crypto wallet apps such as MetaMask that allow you to access your accounts on various blockchains. I was interested in how these apps connect to MetaMask and came across a <a href="https://github.com/giekaton/php-metamask-user-login" rel="noopener" target="_blank">github project</a> using MetaMask as a passwordless user authentication system for web apps and thought this would be a great way to do user logins for some of my applications.</p>
<p>The demo below shows how to connect with a web3 wallet and then authenticate the user to a PHP application by signing a message within the wallet to validate the account.</p>
<p><iframe src="https://demo.gaiterjones.com/web3connect/" style="height:800px;width:100%;" title="DEMO"></iframe></p>
<h1>How it works</h1>
<p>Let&#8217;s look at the workflow used by the client javascript and server php code. </p>
<p>First on the client we try and initialise a web3 provider and connect to a wallet with <code>web3Connect()</code>. This will fetch and initialise account information variables and update the gui. If no provider is found we launch a new <a href="https://github.com/Web3Modal/web3modal" rel="noopener" target="_blank">Web3Modal</a> window which is a single Web3 / Ethereum provider solution for all Wallets.</p>
<p>With the wallet connected and the account public address identified we can offer a user login using the public address as the unique identifier. First we call <code>web3Login()</code> which initiates the backend login process. We are using the <a href="https://github.com/axios/axios" rel="noopener" target="_blank">axios </a>plugin to post the login data to the backend php script which queries an sql database to check if the user account exists / creates a new account. The backend generates an <a href="https://en.wikipedia.org/wiki/Cryptographic_nonce" rel="noopener" target="_blank">Cryptographic nonce</a> which is passed back as a sign request to the client. The client requests that the message be signed in the wallet and the signed response is sent back to the server to be authenticated.</p>
<p>We now have the server generated message, the same message signed by the user and the users public address. The backend performs some cryptographic magic in order to determine if the original message was signed with the same private key to which the public address belongs. The public address also works as a username to identify the account. If the signed message and public address belong to the same private key, it means that the user who is trying to log in is also the owner of the account.</p>
<p>After authentication the backend creates a JSON Web Token (JWT) to authenticate further user requests. PHP Session data is created by the backend which allows the authentication to persist between visits, with the backend using the JWT token to authenticate the user with each page request. The user is now logged in and the client updates the frontend gui accordingly.</p>
<p>To complete the demo a logout button is included to log the user out. In this demo anyone can create a new user account and &#8220;login&#8221;. In practice to restrict user access the backend would have a user approval process to enable new accounts, additionally user groups can be created to apply permissions to the account which are then used by the backend pages to determine which content is available to the user.</p>
<span class="collapseomatic collapse" id="id680b6e88c3092"  tabindex="0" title="Client side js"    >Client side js</span><div id="target-id680b6e88c3092" class="collapseomatic_content ">
[code language=&#8221;javascript&#8221;]
&quot;use strict&quot;;</p>
<p>/**<br />
 *<br />
 * WEB3 Application Wallet Connect / Passwordless client Login<br />
 *<br />
 *<br />
 */</p>
<p> // Unpkg imports<br />
const Web3Modal = window.Web3Modal.default;<br />
const WalletConnectProvider = window.WalletConnectProvider.default;<br />
//const Fortmatic = window.Fortmatic;<br />
const evmChains = window.evmChains;</p>
<p>// Web3modal instance<br />
let web3Modal</p>
<p>// provider instance placeholder<br />
let provider=false;</p>
<p>// Address of the selected account<br />
let selectedAccount;</p>
<p>// web3 instance placeholder<br />
let web3=false;</p>
<p>/**<br />
 * ready to rumble<br />
 */<br />
jQuery(document).ready(function(){</p>
<p>    // initialise<br />
    init();</p>
<p>});</p>
<p>/**<br />
 * Setup the orchestra<br />
 */<br />
async function init() {</p>
<p>    // gui button events<br />
    //<br />
    $(&#8216;#web3connect&#8217;).on(&#8216;click&#8217;, function () {<br />
        web3Connect();<br />
    });</p>
<p>    $(&#8216;#web3login&#8217;).on(&#8216;click&#8217;, function () {<br />
        web3Login();<br />
    });</p>
<p>    $(&#8216;#web3logout&#8217;).on(&#8216;click&#8217;, function () {<br />
        web3Logout();<br />
    });</p>
<p>    //$(&#8216;#web3disconnect&#8217;).on(&#8216;click&#8217;, function () {<br />
    //    web3Disconnect();<br />
    //});</p>
<p>  console.debug(&quot;Initialising Web3&#8230;&quot;);<br />
  console.log(&quot;WalletConnectProvider is&quot;, WalletConnectProvider);<br />
  //console.log(&quot;Fortmatic is&quot;, Fortmatic);<br />
  console.log(&quot;window.web3 is&quot;, window.web3, &quot;window.ethereum is&quot;, window.ethereum);<br />
  console.debug(&#8221; + web3App.loginstate);</p>
<p>  // Check that the web page is run in a secure context,<br />
  // as otherwise MetaMask won&#8217;t be available<br />
  if(location.protocol !== &#8216;https:&#8217;) {<br />
    // https://ethereum.stackexchange.com/a/62217/620<br />
    console.debug(&#8216;HTTPS not available, Doh!&#8217;);<br />
    return;<br />
  }</p>
<p>  // Tell Web3modal what providers we have available.<br />
  // Built-in web browser provider (only one can exist as a time)<br />
  // like MetaMask, Brave or Opera is added automatically by Web3modal<br />
  const providerOptions = {<br />
    walletconnect: {<br />
      package: WalletConnectProvider,<br />
      options: {<br />
        // test key<br />
        infuraId: &quot;8043bb2cf99347b1bfadfb233c5325c0&quot;,<br />
      }<br />
    },</p>
<p>    //fortmatic: {<br />
    //  package: Fortmatic,<br />
    //  options: {<br />
        // TESTNET api key<br />
    //    key: &quot;pk_test_391E26A3B43A3350&quot;<br />
    //  }<br />
    //}<br />
  };</p>
<p>  // https://github.com/Web3Modal/web3modal<br />
  //<br />
  web3Modal = new Web3Modal({<br />
    cacheProvider: true,<br />
    providerOptions<br />
  });</p>
<p>  console.log(&quot;Web3Modal instance is&quot;, web3Modal);</p>
<p>  if (web3Modal.cachedProvider) {<br />
      console.debug(&#8216;Cached Provider found&#8217;);<br />
      web3Connect();<br />
      initPayButton();<br />
  }<br />
}</p>
<p>/**<br />
 * Fetch account data for UI when<br />
 * &#8211; User switches accounts in wallet<br />
 * &#8211; User switches networks in wallet<br />
 * &#8211; User connects wallet initially<br />
 */<br />
async function refreshAccountData() {</p>
<p>  // Disable button while UI is loading.<br />
  // fetchAccountData() will take a while as it communicates<br />
  // with Ethereum node via JSON-RPC and loads chain data<br />
  // over an API call.<br />
  updateGuiButton(&#8216;web3connect&#8217;,&#8217;CONNECTING&#8217;,true);</p>
<p>  await fetchAccountData(provider);</p>
<p>}</p>
<p>/**<br />
 * Get account data<br />
 */<br />
async function fetchAccountData() {</p>
<p>  // init Web3 instance for the wallet<br />
  if (!web3) {web3 = new Web3(provider);}<br />
  console.log(&quot;Web3 instance is&quot;, web3);</p>
<p>  // Get connected chain id from Ethereum node<br />
  const chainId = await web3.eth.getChainId();</p>
<p>  // Load chain information over an HTTP API<br />
  let chainName=&#8217;Unknown&#8217;;</p>
<p>    try {<br />
        const chainData = evmChains.getChain(chainId);<br />
        chainName=chainData.name;<br />
    } catch {<br />
        // error&#8230;<br />
    }</p>
<p>  console.debug(&#8216;Connected to network : &#8216; + chainName + &#8216; [&#8216; + chainId + &#8216;]&#8217;);</p>
<p>  // Get list of accounts of the connected wallet<br />
  const accounts = await web3.eth.getAccounts();</p>
<p>  // MetaMask does not give you all accounts, only the selected account<br />
  console.log(&quot;Got accounts&quot;, accounts);<br />
  selectedAccount = accounts[0];</p>
<p>  web3.eth.defaultAccount = selectedAccount;</p>
<p>  console.debug(&#8216;Selected account : &#8216; + selectedAccount);</p>
<p>  // Go through all accounts and get their ETH balance<br />
  const rowResolvers = accounts.map(async (address) =&gt; {</p>
<p>    web3App.ethAddress = address;</p>
<p>    const balance = await web3.eth.getBalance(address);<br />
    // ethBalance is a BigNumber instance<br />
    // https://github.com/indutny/bn.js/<br />
    const ethBalance = web3.utils.fromWei(balance, &quot;ether&quot;);<br />
    const humanFriendlyBalance = parseFloat(ethBalance).toFixed(4);<br />
    console.debug(&#8216;Wallet balance : &#8216; + humanFriendlyBalance);</p>
<p>  });</p>
<p>  // Because rendering account does its own RPC commucation<br />
  // with Ethereum node, we do not want to display any results<br />
  // until data for all accounts is loaded<br />
  console.debug (&#8216;Waiting for account data&#8230;&#8217;);</p>
<p>  await Promise.all(rowResolvers);</p>
<p>  // Update GUI &#8211; wallet connected<br />
  //<br />
  updateGuiButton(&#8216;web3connect&#8217;,&#8217;CONNECTED&#8217;,true);</p>
<p>  if (web3App.loginstate==&#8217;loggedOut&#8217;)<br />
  {<br />
      updateGuiButton(&#8216;web3login&#8217;,false,false);<br />
  } else {<br />
      updateGuiButton(&#8216;web3login&#8217;,false,true);<br />
  }</p>
<p>  console.debug (&#8216;Wallet connected!&#8217;);</p>
<p>}</p>
<p>/**<br />
 * Connect wallet<br />
 *  when button clicked<br />
 *  or auto if walletConnect cookie set<br />
 */<br />
async function web3Connect() {</p>
<p>  try {</p>
<p>    // if no provider detected use web3 modal popup<br />
    //<br />
    if (!provider)<br />
    {<br />
        console.log(&quot;connecting to provider&#8230;&quot;, web3Modal);<br />
        console.debug(&quot;connecting to provider&#8230;&quot;);<br />
        provider = await web3Modal.connect();<br />
    }</p>
<p>    // Subscribe to accounts change<br />
    provider.on(&quot;accountsChanged&quot;, (accounts) =&gt; {<br />
      fetchAccountData();<br />
      web3Disconnect();<br />
      console.debug(&#8216;Account changed to &#8211; &#8216; + accounts);<br />
    });</p>
<p>    // Subscribe to chainId change<br />
    provider.on(&quot;chainChanged&quot;, (chainId) =&gt; {<br />
      fetchAccountData();<br />
      web3Disconnect();<br />
      console.debug(&#8216;Network changed to &#8211; &#8216; + chainId);<br />
    });</p>
<p>  } catch(e) {<br />
    eraseCookie(&#8216;walletConnect&#8217;);<br />
    console.debug(&quot;Could not get a wallet connection&quot;, e);<br />
    return;<br />
  }</p>
<p>  await refreshAccountData();<br />
}</p>
<p>/**<br />
 * web3 paswordless application login<br />
 */<br />
async function web3Login() {</p>
<p>  if (!provider){web3Connect();}</p>
<p>  let address=web3App.ethAddress;</p>
<p>  address = address.toLowerCase();<br />
  if (!address | address == null) {<br />
    console.debug(&#8216;Null wallet address&#8230;&#8217;);<br />
    return;<br />
  }</p>
<p>  console.debug(&#8216;Login sign request starting&#8230;&#8217;);</p>
<p>  axios.post(<br />
    &quot;/web3login/&quot;,<br />
    {<br />
      request: &quot;login&quot;,<br />
      address: address<br />
    },<br />
    web3App.config<br />
  )<br />
  .then(function(response) {<br />
    if (response.data.substring(0, 5) != &quot;Error&quot;) {<br />
      let message = response.data;<br />
      let publicAddress = address;</p>
<p>      handleSignMessage(message, publicAddress).then(handleAuthenticate);</p>
<p>      function handleSignMessage(message, publicAddress) {<br />
        return new Promise((resolve, reject) =&gt;<br />
          web3.eth.personal.sign(<br />
            web3.utils.utf8ToHex(message),<br />
            publicAddress,<br />
            (err, signature) =&gt; {<br />
              if (err) {<br />
                web3App.loginstate = &quot;loggedOut&quot;;<br />
                console.debug(&#8221; + web3App.loginstate);<br />
              }<br />
              return resolve({ publicAddress, signature });<br />
            }<br />
          )<br />
        );<br />
      }</p>
<p>      function handleAuthenticate({ publicAddress, signature }) {</p>
<p>        try {</p>
<p>            if (!arguments[0].signature){throw &quot;Authentication cancelled, invalid signature&quot;; }<br />
            if (!arguments[0].publicAddress){throw &quot;Authentication cancelled, invalid address&quot;; }</p>
<p>            console.debug(&#8216;Login sign request accepted&#8230;&#8217;);</p>
<p>            axios<br />
              .post(<br />
                &quot;/web3login/&quot;,<br />
                {<br />
                  request: &quot;auth&quot;,<br />
                  address: arguments[0].publicAddress,<br />
                  signature: arguments[0].signature<br />
                },<br />
                web3App.config<br />
              )<br />
              .then(function(response) {</p>
<p>                console.log(response);</p>
<p>                if (response.data[0] == &quot;Success&quot;) {</p>
<p>                  console.debug(&#8216;Web3 Login sign request authenticated.&#8217;);</p>
<p>                  web3App.loginstate = &quot;loggedIn&quot;;<br />
                  console.debug(&#8221; + web3App.loginstate);</p>
<p>                  web3App.ethAddress = address;<br />
                  web3App.publicName = response.data[1];<br />
                  web3App.JWT = response.data[2];</p>
<p>                  updateGuiButton(&#8216;web3login&#8217;,&#8217;Logged in as &#8216; + web3App.publicName,true);<br />
                  updateGuiButton(&#8216;web3logout&#8217;,false,false);</p>
<p>                }<br />
              })<br />
              .catch(function(error) {<br />
                console.error(error);<br />
                updateGuiButton(&#8216;web3login&#8217;,&#8217;LOGIN&#8217;,false);<br />
              });</p>
<p>          } catch(err) {<br />
              console.error(err);<br />
              updateGuiButton(&#8216;web3login&#8217;,&#8217;LOGIN&#8217;,false);<br />
          }<br />
      }</p>
<p>    }<br />
    else {<br />
      console.debug(&quot;Error: &quot; + response.data);<br />
    }</p>
<p>  })<br />
  .catch(function(error) {<br />
    console.error(error);<br />
  });<br />
}</p>
<p>/**<br />
 * web3 Disconnect wallet<br />
 */<br />
async function web3Disconnect()<br />
{<br />
    console.debug(&quot;Killing the wallet connection&quot;);</p>
<p>    // TODO: Which providers have close method?<br />
    if(provider) {<br />
      provider = null;<br />
      await web3Modal.clearCachedProvider();<br />
    }</p>
<p>    localStorage.clear();<br />
    selectedAccount = null;<br />
    updateGuiButton(&#8216;web3connect&#8217;,&#8217;CONNECT&#8217;,false);<br />
    console.debug(&quot;Disconnected&quot;);<br />
}</p>
<p>/**<br />
 * web3 Logout<br />
 */<br />
async function web3Logout()<br />
{<br />
    console.debug(&quot;Clearing server side sessions&#8230;&quot;);<br />
    fetch(&#8216;/web3logout/&#8217;)<br />
        .then((resp) =&gt; resp.json())<br />
        .then(function(data) {</p>
<p>            // logged out<br />
            //<br />
            web3App.loginstate = &quot;loggedOut&quot;;<br />
            web3Disconnect();<br />
            console.debug(&#8221; + web3App.loginstate);<br />
            updateGuiButton(&#8216;web3login&#8217;,&#8217;LOGIN&#8217;,false);<br />
            updateGuiButton(&#8216;web3logout&#8217;,&#8217;LOGOUT&#8217;,true);<br />
        })<br />
        .catch(function(error) {<br />
          console.debug(error);<br />
        });<br />
}</p>
<p>/**<br />
 * pay button<br />
 */<br />
const initPayButton = () =&gt; {<br />
      $(&#8216;#web3pay&#8217;).click(() =&gt; {</p>
<p>        if (!provider){web3Connect();}</p>
<p>        console.debug(&#8216;Requesting transaction signature&#8230;&#8217;);</p>
<p>        const paymentAddress = &#8216;0x&#8217;;<br />
        const paymentAmount = 1;</p>
<p>        web3.eth.sendTransaction({<br />
          to: paymentAddress,<br />
          value: web3.utils.toWei(String(paymentAmount),&#8217;ether&#8217;)<br />
        }, (err, transactionId) =&gt; {<br />
          if  (err) {<br />
            console.debug(&#8216;Payment failed&#8217;, err.message);<br />
          } else {<br />
            console.debug(&#8216;Payment successful&#8217;, transactionId);<br />
          }<br />
        })<br />
      })<br />
    }</p>
<p>/**<br />
 * update gui buttons<br />
 */<br />
function updateGuiButton(element,text,status)<br />
{<br />
    if (text)<br />
    {<br />
        $(&quot;#&quot; + element).val(text);<br />
    }</p>
<p>    // disabled button=true<br />
    // enabled button=false<br />
    if (status==true)<br />
    {<br />
        $(&quot;#&quot; + element).prop(&quot;disabled&quot;,true).css(&quot;cursor&quot;, &quot;default&quot;);<br />
    } else {<br />
        $(&quot;#&quot; + element).prop(&quot;disabled&quot;,false).css(&quot;cursor&quot;, &quot;pointer&quot;);<br />
    }<br />
}</p>
<p>/**<br />
 * debug logger<br />
 */<br />
(function () {<br />
    var logger = document.getElementById(&#8216;log&#8217;);<br />
    console.debug = function () {<br />
      for (var i = 0; i &lt; arguments.length; i++) {</p>
<p>        if (web3App.debug)<br />
        {<br />
            console.log(arguments[i]);</p>
<p>            if (typeof arguments[i] == &#8216;object&#8217;) {<br />
                logger.innerHTML = (JSON &amp;&amp; JSON.stringify ? JSON.stringify(arguments[i], undefined, 2) : arguments[i]) + &#8216;\n&#8217; + logger.innerHTML;<br />
            } else {<br />
                logger.innerHTML = &#8216;Web3App : &#8216; + arguments[i] + &#8216;\n&#8217; + logger.innerHTML;<br />
            }<br />
        }<br />
      }<br />
    }<br />
})();</p>
[/code]
</div>
<p>&#8211;</p>
<p>Demo page design based on a template from <a href="https://html5up.net/" rel="noopener" target="_blank">HTML5UP</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/php-passwordless-user-login-with-web3-connect-and-metamask/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to Configure Varnish for Magento 2</title>
		<link>https://blog.gaiterjones.com/how-to-configure-varnish-for-magento-2/</link>
					<comments>https://blog.gaiterjones.com/how-to-configure-varnish-for-magento-2/#respond</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Tue, 16 Feb 2021 11:28:22 +0000</pubDate>
				<category><![CDATA[Magento2]]></category>
		<category><![CDATA[Varnish FPC]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2430</guid>

					<description><![CDATA[Varnish and Magento 2 go together like Strawberries and Cream &#8211; you just cannot have one without the other. Recently I got really confused about the correct way to configure...<a class="more-link" href="https://blog.gaiterjones.com/how-to-configure-varnish-for-magento-2/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<p>Varnish and Magento 2 go together like <code>Strawberries</code> and <code>Cream</code> &#8211; you just cannot have one without the other.</p>
<p>Recently I got really confused about the correct way to configure Varnish for Magento 2, so for me and anyone else confused about the configuration here is the definitive guide to configuring Varnish for Magento 2.</p>
<h1>The Definitive Guide to Configuring Varnish for Magento 2</h1>
<p>To configure Varnish you need to know:</p>
<ul>
<li>Varnish server name</li>
<li>Varnish listener TCP port &#8211; defaults to 6081</li>
<li>Magento content server name</li>
<li>Magento content server TCP port</li>
</ul>
<p>If you are working with a single host the server name for Varnish and Magento will be localhost (127.0.0.1), if you are working in a Docker environment the server name for Varnish and Magento will be the container names of the Varnish and Magento web/content server services.</p>
<p>There are two areas in Magento where you must configure Varnish settings:</p>
<ol>
<li>Magento Core Config : <code>app/etc/env.php</code></li>
<li>Magento admin <code>Stores -> Configuration -> System -> Full Page Cache</code></li>
</ol>
<h2>Core Config</h2>
<p>The core config for Varnish in <code>app/etc/env.php</code> looks something like this:</p>
<pre class="brush: php; title: ; notranslate">
    'http_cache_hosts' =&gt; &#x5B;
        &#x5B;
            'host' =&gt; 'varnish_server_hostname',
            'port' =&gt; '6081'
        ]
    ],
</pre>
<ul>
<li><strong>HOST</strong>=hostname / ip address of Varnish server</li>
<li><strong>PORT</strong>=TCP listener port of Varnish server</li>
</ul>
<p>You can configure these settings from the command line using:</p>
<p><code>php bin/magento setup:config:set --http-cache-hosts=varnish_server_hostname:6081</code></p>
<h2>Admin Config</h2>
<p>The admin config for Varnish in <code>Stores -> Configuration -> System -> Full Page Cache</code> looks something like this:</p>
<figure id="attachment_2432" aria-describedby="caption-attachment-2432" style="width: 550px" class="wp-caption aligncenter"><img fetchpriority="high" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2021/02/varnish2-620x340.jpg" alt="Magento 2 Varnish FPC Admin Configuration" width="550" height="302" class="size-large wp-image-2432" srcset="https://blog.gaiterjones.com/wp-content/uploads/2021/02/varnish2-620x340.jpg 620w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/varnish2-440x241.jpg 440w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/varnish2-768x421.jpg 768w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/varnish2-760x417.jpg 760w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/varnish2-550x302.jpg 550w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/varnish2.jpg 800w" sizes="(max-width: 550px) 100vw, 550px" /><figcaption id="caption-attachment-2432" class="wp-caption-text">Magento 2 Varnish FPC Admin Configuration</figcaption></figure>
<ul>
<li><strong>ACCESS LIST</strong>=hostname / ip address of Magento content server/s</li>
<li><strong>BACKEND HOST</strong>=hostname / ip address of Magento content server/s</li>
<li><strong>BACKEND PORT</strong>=TCP listener port of Magento content server</li>
<li><strong>EXPORT CONFIGURATION</strong>=Click here to export a the Varnish VCL file</li>
</ul>
<p><strong>Note that the Varnish Configuration section settings here are only used to generate the Varnish VCL file.</strong></p>
<p>If you are using Docker set the acl purge list in the Varnish VCL to all Docker private networks.</p>
<pre class="brush: plain; title: ; notranslate">
# purge set to docker nets
# 172.0.0.0/12 192.168.0.0/16
acl purge {
    &quot;172.16.0.0&quot;/12;
    &quot;192.168.0.0&quot;/16;
}
</pre>
<h2>Troubleshooting</h2>
<p>You can verify your Varnish configuration with <a href="https://github.com/netz98/n98-magerun2" rel="noopener" target="_blank">n98-magerun2</a>. Use the command:</p>
<p><code>n98-magerun2.phar config:show | grep full_page_cache</code></p>
<p>You should see something like the following where magento2_php-apache_1is the hostname of your Magento 2 content server and backend_port is the tcp port of the content (Magento) server</p>
<pre class="brush: plain; title: ; notranslate">
system/full_page_cache/caching_application - 2
system/full_page_cache/varnish/access_list - magento2_php-apache_1
system/full_page_cache/varnish/backend_host - magento2_php-apache_1
system/full_page_cache/varnish/backend_port - 8080
system/full_page_cache/varnish/grace_period - 300
</pre>
<p><strong>Don’t get the ports mixed up :</strong></p>
<ul>
<li>By default Varnish is configured to listen for incoming external client http requests on TCP 6081.</li>
<li>The backend_port configured in admin is only used for the vcl config generation.</li>
<li>The env.php http_cache_hosts port is the port used to communicate with varnish.</li>
</ul>
<p>To confirm your Varnish cache is working examine the headers returned by your Varnish server when browsing Magento frontend pages. You can also inspect the headers using curl</p>
<p><code>curl -I -H "host: magento2.gaiterjones.com" 127.0.0.1:80</code></p>
<pre class="brush: plain; title: ; notranslate">
X-Varnish: 762904605
Age: 0
X-Magento-Cache-Debug: HIT
Pragma: no-cache
Expires: -1
Cache-Control: no-store
</pre>
<p>Here we can see the X-Magento-Cache-Debug header showing a cache hit. Note &#8211; this header will be disabled in production mode.</p>
<p><em>Remember that Varnish has no support for TLS connections over HTTPS.</em> To use an encrypted TLS connection to Magento 2 with Varnish FPC you need to use a frontend proxy such as NGINX.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/how-to-configure-varnish-for-magento-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Magento 2 Asynchronous Message Queue Management with RabbitMQ</title>
		<link>https://blog.gaiterjones.com/magento-2-asynchronous-message-queue-management/</link>
					<comments>https://blog.gaiterjones.com/magento-2-asynchronous-message-queue-management/#respond</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Sat, 13 Feb 2021 18:00:15 +0000</pubDate>
				<category><![CDATA[Magento2]]></category>
		<category><![CDATA[Message Queues]]></category>
		<category><![CDATA[AMQP]]></category>
		<category><![CDATA[Magento 2 Message Queues]]></category>
		<category><![CDATA[RabbitMQ]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2387</guid>

					<description><![CDATA[The Magento Message Queue Framework was made available in the Open Source version of Magento with version 2.3 and provides a standards based system for modules to publish messages to...<a class="more-link" href="https://blog.gaiterjones.com/magento-2-asynchronous-message-queue-management/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<p>The Magento Message Queue Framework was made available in the Open Source version of Magento with version 2.3 and provides a standards based system for modules to publish messages to queues and also defines the consumers that will receive the messages asynchronously.</p>
<h1>What are Asynchronous Message Queues?</h1>
<p>The normal way for a module to process data caused by some kind of front or backend action is to create an observer that listens for the specific Magento event and then processes the event data in some way. For example if you want to process order data whenever an order is placed, you would create a custom module with an order event observer. When a customer places an order the event will be fired and your module can process the order data.</p>
<p>This event type processing occurs synchronously with the event. This can be a problem if your process is intensive and takes some time to complete, or if for some reason it causes an error. Synchronous processing of events may cause delays for the customer in frontend processes.</p>
<p>Asynchronous Message Queues allow your event to be placed in a queuing system and processed by your modules consumer <em>as soon as possible</em>.</p>
<p>The Magento Message Queue Framework consists of the following components:</p>
<ul>
<li>Publisher<br />
A publisher is a component that sends messages to an exchange.
        </li>
<li>Exchange <br />
An exchange receives messages from publishers and sends them to queues.
        </li>
<li>Queue<br />
A queue is a buffer that stores messages.
        </li>
<li>Consumer<br />
A consumer receives messages. It knows which queue to <em>consume</em>.
        </li>
</ul>
<p>By default Magento 2 uses the MySQL database as the Exchange and Queue system for messages. It does this with a MySQL adapter, Message data is stored in the <code>queue, queue_message, queue_message_status</code> tables. Magento uses the cron job <code>consumers_runner</code> to manage queued messages by starting (or restarting) message consumers.</p>
<p>The fastest this can happen with the Magento cron system is once every 60 seconds. The job is configured in the <em>MessageQueue </em>module. </p>
<pre class="brush: plain; title: ; notranslate">
job name=&quot;consumers_runner&quot; instance=&quot;Magento\MessageQueue\Model\Cron\ConsumersRunner&quot; method=&quot;run&quot;&gt;
    &lt;schedule&gt;* * * * *&lt;/schedule&gt;
&lt;/job&gt;
</pre>
<p>I recommend looking at <code>Magento\MessageQueue\Model\Cron\ConsumersRunner</code> to understand how the default Magento consumers are managed.</p>
<p>If you want to list all the default consumers available in Magento 2 use the command <code>bin/magento queue:consumers:list</code></p>
<span class="collapseomatic collapse" id="id680b6e88c620d"  tabindex="0" title="Default message consumers/queues in Magento 2.4.2"    >Default message consumers/queues in Magento 2.4.2</span><div id="target-id680b6e88c620d" class="collapseomatic_content ">
[text]
product_action_attribute.update<br />
product_action_attribute.website.update<br />
exportProcessor<br />
inventory.source.items.cleanup<br />
inventory.mass.update<br />
inventory.reservations.cleanup<br />
inventory.reservations.update<br />
codegeneratorProcessor<br />
media.storage.catalog.image.resize<br />
inventory.reservations.updateSalabilityStatus<br />
inventory.indexer.sourceItem<br />
inventory.indexer.stock<br />
media.content.synchronization<br />
media.gallery.renditions.update<br />
media.gallery.synchronization<br />
async.operations.all<br />
[/text]
</div>
<p>&nbsp;</p>
<h3>Using the Magento database for messaging is not very scalable</h3>
<blockquote><p>RabbitMQ should be used whenever possible.</p></blockquote>
<h1>Converting Magento 2 Message Queues to Rabbit MQ AMQP</h1>
<p><a href="http://rabbitmq.com" rel="noopener" target="_blank">RabbitMQ</a> is an open source message broker system and the Magento Message Queue Framework has built in support for RabbitMQ as a scalable platform for sending and receiving messages. RabbitMQ is based on the Advanced Message Queuing Protocol (AMQP) 0.9.1 specification.</p>
<p>Configuring Magento 2 to use RabbitMQ requires the addition of a new queue node in <code>app/etc/env.php</code></p>
<pre class="brush: plain; title: ; notranslate">
    'queue' =&gt; &#x5B;
        'amqp' =&gt; &#x5B;
            'host' =&gt; '&lt;rabbitmq_host&gt;',
            'port' =&gt; '&lt;rabbitmq_port&gt;',
            'user' =&gt; '&lt;rabbitmq_user&gt;',
            'password' =&gt; '&lt;rabbitmq_pass&gt;',
            'virtualhost' =&gt; '/',
            'ssl' =&gt; false
        ],
     ]
</pre>
<p>Enabling message queues for RabbitMQ AMQP is simply a case of changing the configuration of the queue from DB to AMQP.</p>
<p><code>connection="amqp"</code></p>
<p>One built in message queue is by default configured to use AMQP this is the <code>async.operations.all</code> queue. If you have successfully configured RabbitMQ you should see this queue in the admin interface</p>
<figure id="attachment_2391" aria-describedby="caption-attachment-2391" style="width: 600px" class="wp-caption aligncenter"><img decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_1.jpg" alt="Default AMQP Message Queue" width="600" height="199" class="size-full wp-image-2391" srcset="https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_1.jpg 600w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_1-440x146.jpg 440w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_1-550x182.jpg 550w" sizes="(max-width: 600px) 100vw, 600px" /><figcaption id="caption-attachment-2391" class="wp-caption-text">Default AMQP Message Queue</figcaption></figure>
<p>When creating message queues for new modules you should configure them to use AMQP, check out <a href="https://github.com/gaiterjones/Magento2_Gaiterjones_Rabbitmq">this module</a> for an example module using a product save event message.</p>
<p>If you want to convert existing MySQL DB message queues to use AMQP you can accomplish this using extra nodes in the <code>env.php</code> queue configuration. This example is taken from the <a href="http://Magento\MessageQueue\Model\Cron\ConsumersRunner">official documentation</a> for <code>product_action_attribute.update</code> queue:</p>
<pre class="brush: plain; title: ; notranslate">
'queue' =&gt; &#x5B;
    'topics' =&gt; &#x5B;
        'product_action_attribute.update' =&gt; &#x5B;
            'publisher' =&gt; 'amqp-magento'
        ]
    ],
    'config' =&gt; &#x5B;
        'publishers' =&gt; &#x5B;
            'product_action_attribute.update' =&gt; &#x5B;
                'connections' =&gt; &#x5B;
                    'amqp' =&gt; &#x5B;
                        'name' =&gt; 'amqp',
                        'exchange' =&gt; 'magento',
                        'disabled' =&gt; false
                    ],
                    'db' =&gt; &#x5B;
                        'name' =&gt; 'db',
                        'disabled' =&gt; true
                    ]
                ]
            ]
        ]
    ],
    'consumers' =&gt; &#x5B;
        'product_action_attribute.update' =&gt; &#x5B;
            'connection' =&gt; 'amqp',
        ],
    ],
],
</pre>
<p>In theory you can try and convert all Magento 2 MySQL message queues to AMQP RabbitMQ message queues. If you do this you will also probably want to create your own external consumers to process RabbitMQ message queues more efficiently.</p>
<p>Keep in mind that if you create external message queue consumers you should ensure that the consumer processes are always running. There are a few ways to accomplish this for example by using a <a href="http://supervisord.org/" rel="noopener" target="_blank">supervisord </a>process control system. If you are working with a Docker environment I recommend creating one or more consumer containers to manage Magento 2 AMQP messages.</p>
<p>You can configure the default Magento 2 <code>cron_runners_runner</code> cron job via <code>env.php</code> settings</p>
<pre class="brush: plain; title: ; notranslate">
    'cron_consumers_runner' =&gt; &#x5B;
        'cron_run' =&gt; true,
        'max_messages' =&gt; 20000,
        'consumers' =&gt; &#x5B;
            'consumer1',
            'consumer2',
        ]
    ],
</pre>
<p>Note that if you set <code>cron_run</code> here to <code>false</code> you are completely disabling the default consumer cron job in Magento. If you are using external consumers for some or all message queues <strong>think carefully</strong> before completely disabling this cron job.</p>
<p><em>Note &#8211; during testing I was unable to convert some existing Magento 2.4.2 MySQL queues to AMQP</em></p>
<h1>Docker Consumers and Message Management</h1>
<p>I like to use <a href="https://github.com/gaiterjones/docker-magento2/tree/master/magento2/consumer" rel="noopener" target="_blank">dedicated Docker containers</a> to manage message queue consumer processes in my Magento 2 Docker environment. In theory a container should manage a single consumer, in practice it can be easier to run multiple consumers in one container. The container needs to know which consumers to start, and also needs to monitor consumer processes to ensure that they remain running.</p>
<p>To manage this process and make it easier to convert multiple message queues from MySQL db to AMQP I created a <a href="https://github.com/gaiterjones/Magento2_MessageManager" rel="noopener" target="_blank">Message Manager module</a>.</p>
<p>This module allows you to convert some or all queues to AMQP, automatically updating the <code>env.php</code> settings to configure the changes required for each module. The module also feeds data to my Docker container telling the container which consumer processes it should manage.</p>
<p>After installing the module you can display all existing message consumers with:</p>
<p><code>bin/magento messagemanager:getconsumers</code></p>
<p>This will return an array of consumers showing the queue name and the current connection method &#8211; <strong>db</strong> for MySQL, &#8211; <strong>amqp</strong> for RabbitMQ AMQP.</p>
<p>To convert specific queues to AMQP you can define a whitelist in <code>Gaiterjones\MessageManager\Helper\Data</code></p>
<pre class="brush: php; title: ; notranslate">
    public function whitelist(): array
    {
        return array(
            'gaiterjones_message_manager',
            'product_action_attribute.update'
        );
    }
</pre>
<p>Here I am specifying two queues that I want to use with RabbitMQ AMQP. I know <code>gaiterjones_message_manager</code> is already configured for AMQP and I want to convert <code>product_action_attribute.update</code> from MySQL to AMQP.</p>
<p>To do this I can use the getconfig command. First check your current AMQP config with <code>bin/magento messagemanager:getconfig</code> ensure that your RabbitMQ server is configured.</p>
<pre class="brush: php; title: ; notranslate">
    &#x5B;amqp] =&gt; Array
        (
            &#x5B;host] =&gt; magento2_rabbitmq_1
            &#x5B;port] =&gt; 5672
            &#x5B;user] =&gt; blah
            &#x5B;password] =&gt; blah
            &#x5B;virtualhost] =&gt; /
        )
</pre>
<p>Next display the configuration required to convert your whitelisted queues to AMQP</p>
<p><code>bin/magento messagemanager:getconfig --buildconfig --whitelist</code></p>
<p>(To display the configuration for all MySQL configured queues use <code>bin/magento messagemanager:getconfig --buildconfig</code>)</p>
<p>After reviewing the configuration use the <code>saveconfig</code> switch to save this configuration to <code>env.php</code> :</p>
<p><code>bin/magento messagemanager:getconfig --buildconfig --whitelist --saveconfig</code></p>
<p><strong>Make sure you backup</strong> <code>env.php</code> before running this command!</p>
<p>After changing the config run <code>setup:upgrade</code> to reconfigure the module queues. If the queue config creates an error restore your <code>env.php</code> settings and run <code>setup:upgrade</code> again to restore the configuration.</p>
<p>Now you should see the converted queues in RabbitMQ admin.</p>
<figure id="attachment_2393" aria-describedby="caption-attachment-2393" style="width: 600px" class="wp-caption aligncenter"><img decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_2.jpg" alt="MySQL queues converted to AMQP and visible in RabbitMQ" width="600" height="94" class="size-full wp-image-2393" srcset="https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_2.jpg 600w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_2-440x69.jpg 440w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_2-550x86.jpg 550w" sizes="(max-width: 600px) 100vw, 600px" /><figcaption id="caption-attachment-2393" class="wp-caption-text">MySQL queues converted to AMQP and visible in RabbitMQ</figcaption></figure>
<p>The <code>gaiterjones_message_manager</code> queue is installed with the module, to test RabbitMQ is working use the test queue command <code>bin/magento messagemanager:testqueue</code></p>
<p>You should see a new message created in RabbitMQ.</p>
<figure id="attachment_2394" aria-describedby="caption-attachment-2394" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_3.jpg" alt="RabbitMQ Message Queue Test" width="600" height="394" class="size-full wp-image-2394" srcset="https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_3.jpg 600w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_3-440x289.jpg 440w, https://blog.gaiterjones.com/wp-content/uploads/2021/02/mq_3-550x361.jpg 550w" sizes="auto, (max-width: 600px) 100vw, 600px" /><figcaption id="caption-attachment-2394" class="wp-caption-text">RabbitMQ Message Queue Test</figcaption></figure>
<h2>Summary</h2>
<p>It is very likely that future versions of Magento 2 will stop using MySQL as a messaging broker. In the same way that Search was moved to Elasticsearch we may have to use RabbitMQ in the future for all Magento async messaging.</p>
<p>Message queues are cool! If you are building production Magento 2 sites in a Docker environment it makes sense to start using RabbitMQ and external consumers now.</p>
<h2>More Reading</h2>
<p>In conclusion I recommend reading through the official documentation for more information on this subject.</p>
<ul>
<li><a href="https://devdocs.magento.com/guides/v2.4/config-guide/mq/manage-message-queues.html#start-message-queue-consumers" rel="noopener" target="_blank">Manage Message Queues</a></li>
<li><a href="https://devdocs.magento.com/guides/v2.4/config-guide/mq/rabbitmq-overview.html" rel="noopener" target="_blank">Message Queues Overview</a></li>
<li><a href="https://devdocs.magento.com/guides/v2.4/config-guide/prod/config-reference-envphp.html#consumers_wait_for_messages" rel="noopener" target="_blank">consumers_wait_for_messages</a></li>
<li><a href="https://devdocs.magento.com/guides/v2.4/extension-dev-guide/message-queues/message-queues.html" rel="noopener" target="_blank">Message Queues</a></li>
</ul>
<h4>Credits</h4>
<p>Thanks to <a href="https://magento.stackexchange.com/users/35330/diana-botean" rel="noopener" target="_blank">Diana Botean</a> for her helpful comments.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/magento-2-asynchronous-message-queue-management/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Magento 2 Custom Customer Registration Checkbox and Text Attributes</title>
		<link>https://blog.gaiterjones.com/magento-2-custom-customer-registration-checkbox-and-text-attributes/</link>
					<comments>https://blog.gaiterjones.com/magento-2-custom-customer-registration-checkbox-and-text-attributes/#respond</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Fri, 18 Dec 2020 14:04:31 +0000</pubDate>
				<category><![CDATA[Magento2]]></category>
		<category><![CDATA[customr registration attribute]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2371</guid>

					<description><![CDATA[I spent many hours recently trying to figure out why a custom Magento 2 customer registration attribute was not working only to find that a relatively simple mistake was the...<a class="more-link" href="https://blog.gaiterjones.com/magento-2-custom-customer-registration-checkbox-and-text-attributes/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<p>I spent many hours recently trying to figure out why a custom Magento 2 customer registration attribute was not working only to find that a relatively simple mistake was the culprit.</p>
<p>The attribute appeared to be created correctly in the <code>eav_attribute</code> database table, but the frontend value was not being saved to the database when the customer registered.</p>
<p>The attribute was a checkbox, which has a boolean true/false value. As I set the checkbox to be checked by default, my mistake was simply not to set a default value which meant that no value for the custom attribute was being passed via the registration form to the backend customer registration / creation process.</p>
<pre class="brush: php; title: ; notranslate">
    &lt;div class=&quot;field gj_custom_boolean_attribute&quot;&gt;
        &lt;label for=&quot;gj_custom_boolean_attribute&quot; class=&quot;label&quot;&gt;&lt;span&gt;&lt;?php /* @escapeNotVerified */
                echo __('Customer Custom Boolean Attribute') ?&gt;&lt;/span&gt;
        &lt;/label&gt;
        &lt;div class=&quot;control&quot;&gt;
            &lt;input type=&quot;checkbox&quot; checked name=&quot;gj_custom_boolean_attribute&quot; id=&quot;gj_custom_boolean_attribute&quot; title=&quot;&lt;?php /* @escapeNotVerified */
            echo __('Customer Custom Boolean Attribute') ?&gt;&quot; class=&quot;input-text&quot; autocomplete=&quot;off&quot; value=&quot;1&quot;&gt;
        &lt;/div&gt;
    &lt;/div&gt;
</pre>
<p>In case anyone else is trying to create a custom checkbox (boolean) attribute and experiencing the same mind numbingly annoying problem here is a module with a demonstration of two custom Magento 2 attributes &#8211; checkbox and text input that demonstrates the correct working code.</p>
<figure id="attachment_2374" aria-describedby="caption-attachment-2374" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/12/customerregistration_1.jpg" alt="" width="600" height="242" class="size-full wp-image-2374" srcset="https://blog.gaiterjones.com/wp-content/uploads/2020/12/customerregistration_1.jpg 600w, https://blog.gaiterjones.com/wp-content/uploads/2020/12/customerregistration_1-440x177.jpg 440w, https://blog.gaiterjones.com/wp-content/uploads/2020/12/customerregistration_1-550x222.jpg 550w" sizes="auto, (max-width: 600px) 100vw, 600px" /><figcaption id="caption-attachment-2374" class="wp-caption-text">Magento 2 Custom Customer Registration Attributes</figcaption></figure>
<p><a href="https://github.com/gaiterjones/Magento2_Gaiterjones_CustomerRegistration" rel="noopener" target="_blank">https://github.com/gaiterjones/Magento2_Gaiterjones_CustomerRegistration</a></p>
<p>The module creates two attributes:</p>
<ul>
<li>gj_custom_boolean_attribute &#8211; a sample boolean checkbox attribute</li>
<li>gj_custom_text_attribute &#8211;  a sample text attribute</li>
</ul>
<p>The additional registration html is in the <code>additionalinfocustomer.phtml</code> template file. Note here that the checkbox requires a default value</p>
<p><code>value="1"</code></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/magento-2-custom-customer-registration-checkbox-and-text-attributes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to Improve Magento 2 ElasticSearch Catalog Search Results and Relevance</title>
		<link>https://blog.gaiterjones.com/how-to-improve-magento-2-elasticsearch-catalog-search-results-and-relevance/</link>
					<comments>https://blog.gaiterjones.com/how-to-improve-magento-2-elasticsearch-catalog-search-results-and-relevance/#respond</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Thu, 20 Aug 2020 17:15:25 +0000</pubDate>
				<category><![CDATA[ElasticSearch]]></category>
		<category><![CDATA[Magento2]]></category>
		<category><![CDATA[elasticsearch]]></category>
		<category><![CDATA[magento2]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2275</guid>

					<description><![CDATA[TL;DR Migrating the Magento 2 catalog search engine to Smile ElasticSuite will resolve pretty much all the issues you might be experiencing with Magento 2 native ElasticSearch catalog search so...<a class="more-link" href="https://blog.gaiterjones.com/how-to-improve-magento-2-elasticsearch-catalog-search-results-and-relevance/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<h1>TL;DR</h1>
<p>Migrating the Magento 2 catalog search engine to <a href="https://github.com/Smile-SA/elasticsuite">Smile ElasticSuite</a> will resolve pretty much all the issues you might be experiencing with Magento 2 native ElasticSearch catalog search so go ahead and <a href="#elasticsuite">Jump to the ElasticSuite installation.</a></p>
<p>If you are new to ElasticSearch or want to find out how to customise ElasticSearch in Magento 2 read on!</p>
<h1>Magento Catalog Search</h1>
<p>Up until version 2.3 the default catalog search engine for Magento used the MySql Magento database. Using MySql for search was adequate but it lacked the features and scalability of enterprise search solutions. In version 2.3 Magento built in support for ElasticSearch as the catalog search engine and announced in 2019 that MySql search would be deprecated. As of version 2.4 in July 2020 the MySql catalog search engine was removed completely from Magento 2.</p>
<figure id="blog-es_5" aria-describedby="caption-blog-es-5" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_5.png" alt="" width="393" height="280" class="aligncenter size-full wp-image-2301" /><figcaption id="caption-blog-es-5" class="wp-caption-text"><br />
MySql catalog search deprecation notice<br /></figcaption></figure>
<p>Native support for ElasticSearch was good news for Merchants, Elasticsearch is a java based open-source, RESTful, distributed search and analytics engine built on Apache Lucene. Since its release in 2010, Elasticsearch has quickly become the most popular search engine.</p>
<p>It&#8217;s worth mentioning that ElasticSearch in Magento 2 is not just used for user full text search queries, the catalog search engine is responsible for returning all catalog queries including category products and filtered navigation product lists.</p>
<p>For customers ElasticSearch should provide faster and more <strong>relevant</strong> search experiences &#8211; the problem for merchants is that out of the box Magento 2 ElasticSearch just doesn&#8217;t do this &#8211; catalog search results and relevance have a tendency to be extremely poor. The built in search struggles to provide accurate and relevant results for simple queries such as SKUs.</p>
<p>Whilst MySql catalog search had some admin options to refine search results, there are no options available to customise catalog search with ElasticSearch. ElasticSearch is a great search engine but the native Magento 2 catalog full text search implementation is very disappointing.</p>
<p>Let&#8217;s look at ways to customise ElasticSearch catalog search in Magento using your own module to improve some areas of search relevance.</p>
<h1>Simple SKU Search</h1>
<p>Poor search results or search relevance with native Magento ElasticSearch is very apparent when searching for SKUs. Let&#8217;s look at a simple SKU search for one of the sample products provided in the Magento 2 sample data.</p>
<figure id="blog-es_1" aria-describedby="caption-blog-es-1" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_1.jpg" alt="" width="454" height="318" class="aligncenter wp-image-2281" /><figcaption id="caption-blog-es-1" class="wp-caption-text"><br />
SKU Search for Magento 2 sample product<br /></figcaption></figure>
<p>Article MH03 is a range of Hoodies. Searching for &#8216;MH03&#8217; correctly returns all 16 products. But what if you want to search for MH03-XL?</p>
<figure id="blog-es_2-2" aria-describedby="caption-blog-es-2-2" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_2-2.jpg" alt="" width="446" height="253" class="aligncenter wp-image-2283" /><figcaption id="caption-blog-es-2-2" class="wp-caption-text"><br />
Refined SKU Search for Magento 2 sample data<br /></figcaption></figure>
<p>Here we see that 112 items are returned when in fact only the first 3 were 100% matches for the search term. Native search really struggles with search terms containing special characters such as the hyphen commonly used in SKUs resulting in extremely poor search results. To look at why we are seeing so many results returned we need to look at the relevance score of the search results.</p>
<h1>Customise Elastic Search</h1>
<p>To capture the data returned by an ElasticSearch frontend full text search query we need to create a debug plugin for <code>Magento\Elasticsearch\SearchAdapter\ResponseFactory</code> that will let us analyse the search data and log it.</p>
<p><code>&lt;type name="Magento\Elasticsearch\SearchAdapter\ResponseFactory"&gt;</code><br />
<code>&lt;plugin disabled="false" sortOrder="2" name="gaiterjones_elasticsearch_queryresultsdebug" type="Gaiterjones\Elasticsearch\Plugin\QueryResultsDebug"/&gt;</code><br />
<code>&lt;/type&gt;</code></p>
<div style="padding-bottom: 20px;">
<span class="collapseomatic collapse" id="id680b6e88cb310"  tabindex="0" title="beforeCreate plugin for SearchAdapterResponseFactory"    >beforeCreate plugin for SearchAdapterResponseFactory</span><div id="target-id680b6e88cb310" class="collapseomatic_content ">
[code language=&#8221;php&#8221;]
    public function beforeCreate(\Magento\Elasticsearch\SearchAdapter\ResponseFactory $subject, $result)<br />
    {<br />
       if($this-&gt;debug)<br />
       {<br />
           if(!is_array($result) || empty($result)) {return false;}</p>
<p>           $scores=array();</p>
<p>           foreach ($result[&#8216;documents&#8217;] as $rawDocument) {<br />
               $this-&gt;logger-&gt;debug(&#8216;ELASTIC_SEARCH_QUERY_RESULTS_DEBUG&#8217;,$rawDocument);<br />
               array_push($scores,$rawDocument[&#8216;_score&#8217;]);<br />
           }</p>
<p>           if (count($result[&#8216;documents&#8217;]) &gt; 0)<br />
           {<br />
              $_debug=array(<br />
                   &#8216;results&#8217; =&gt; count($result[&#8216;documents&#8217;]),<br />
                   &#8216;scores&#8217; =&gt; $scores,<br />
                   &#8216;min_relevance_score&#8217; =&gt; $this-&gt;configuration-&gt;getMinScore(),<br />
                   &#8216;min_score&#8217; =&gt; min($scores),<br />
                   &#8216;max_score&#8217; =&gt; max($scores)<br />
               );</p>
<p>               $this-&gt;logger-&gt;debug(&#8216;ELASTIC_SEARCH_QUERY_RESULTS_DEBUG&#8217;,$_debug);<br />
           }<br />
       }<br />
    }<br />
[/code]
</div>
</p></div>
<p>The plugin dumps the search data to the debug log in <code>var/log</code>, this allows us to look more closely at the ElasticSearch results for the MH03-XL SKU full text search :</p>
<pre>Array
(
    [results] =&gt; 112
    [scores] =&gt; Array
        (
            [0] =&gt; 40.57959
            [1] =&gt; 40.57959
            [2] =&gt; 40.57959
            [3] =&gt; 29.099976
            [4] =&gt; 12.694372
            SNIP...
            [108] =&gt; 8.300152
            [109] =&gt; 8.300152
            [110] =&gt; 7.9659967
            [111] =&gt; 7.9659967
        )

    [min_relevance_score] =&gt; 1
    [min_score] =&gt; 7.9659967
    [max_score] =&gt; 40.57959
)
</pre>
<p>The debug shows the ElasticSearch rawdocument score  for the 112 search results, you can see that the score value for the search ranges from 7.9 to 40.5 with the most relevant results having a higher score. If we were to define a minimum relevance score of 40 the search results would be much more accurate.</p>
<p>We can do this with another plugin :</p>
<p><code>&lt;type name="Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Mapper"&gt;</code><br />
<code>&lt;plugin disabled="false" sortOrder="1" name="gaiterjones_elasticsearch_searchadapter_mapperplugin" type="Gaiterjones\Elasticsearch\Plugin\Elasticsearch5\SearchAdapter\MapperPlugin"/&gt;</code><br />
<code>&lt;/type&gt;</code></p>
<div style="padding-bottom: 20px;">
<span class="collapseomatic collapse" id="id680b6e88cb346"  tabindex="0" title="aroundBuildQuery plugin for SearchAdapterMapperPlugin"    >aroundBuildQuery plugin for SearchAdapterMapperPlugin</span><div id="target-id680b6e88cb346" class="collapseomatic_content ">
[code language=&#8221;php&#8221;]
    public function aroundBuildQuery(<br />
        Mapper $subject,<br />
        callable $proceed,<br />
        RequestInterface $request<br />
    ) {<br />
        $searchQuery = $proceed($request);</p>
<p>        if ($request-&gt;getName() === &#8216;quick_search_container&#8217;) {<br />
            $searchQuery[&#8216;body&#8217;][&#8216;min_score&#8217;] = $this-&gt;configuration-&gt;getMinScore();<br />
        }</p>
<p>        return $searchQuery;<br />
    }<br />
[/code]
</div>
</p></div>
<p>Here we set a <code>min_score</code> value for the search query. Setting this to <em>40</em> would return just three results for the MH03-XL SKU search.</p>
<figure id="blog-es_3" aria-describedby="caption-blog-es-3" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_3.jpg" alt="" width="600" height="315" class="aligncenter size-full wp-image-2285" /><figcaption id="caption-blog-es-3" class="wp-caption-text"><br />
SKU Search for Magento 2 sample products with min_score value<br /></figcaption></figure>
<p>This looks much better, we can improve the relevance of the search results by filtering out results that have a low ElasticSearch score. The tricky part here is deciding what the minimum score value should be &#8211; it can be difficult to find a value that works well for different search queries.</p>
<p>Another useful ElasticSearch customisation is changing the ngram values when indexing the catalog. The <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-ngram-tokenizer.html">ngram tokenizer</a> helps break search terms up into smaller words. We can change the ngram value with another plugin</p>
<p><code>&lt;type name="Magento\Elasticsearch\Model\Adapter\Index\Builder"&gt;</code><br />
<code>&lt;plugin disabled="false" sortOrder="3" name="gaiterjones_elasticsearch_indexbuilder" type="Gaiterjones\Elasticsearch\Plugin\IndexBuilder"&gt;&lt;/plugin&gt;</code><br />
<code>&lt;/type&gt;</code></p>
<div style="padding-bottom: 20px;">
<span class="collapseomatic collapse" id="id680b6e88cb37b"  tabindex="0" title="plugin afterBuild method for IndexBuilder"    >plugin afterBuild method for IndexBuilder</span><div id="target-id680b6e88cb37b" class="collapseomatic_content ">
[code language=&#8221;php&#8221;]
    public function afterBuild(\Magento\Elasticsearch\Model\Adapter\Index\Builder $subject, $result)<br />
    {<br />
        $likeToken = $this-&gt;getLikeTokenizer();<br />
        $result[&#8216;analysis&#8217;][&#8216;tokenizer&#8217;] = $likeToken;<br />
        $result[&#8216;analysis&#8217;][&#8216;filter&#8217;][&#8216;trigrams_filter&#8217;] = [<br />
            &#8216;type&#8217; =&gt; &#8216;ngram&#8217;,<br />
            &#8216;min_gram&#8217; =&gt; 3,<br />
            &#8216;max_gram&#8217; =&gt; 3<br />
        ];<br />
        $result[&#8216;analysis&#8217;][&#8216;analyzer&#8217;][&#8216;my_analyzer&#8217;] = [<br />
            &#8216;type&#8217; =&gt; &#8216;custom&#8217;,<br />
            &#8216;tokenizer&#8217; =&gt; &#8216;standard&#8217;,<br />
            &#8216;filter&#8217; =&gt; [<br />
                &#8216;lowercase&#8217;, &#8216;trigrams_filter&#8217;<br />
            ]
        ];<br />
        return $result;<br />
    }</p>
<p>    protected function getLikeTokenizer()<br />
    {<br />
        $tokenizer = [<br />
            &#8216;default_tokenizer&#8217; =&gt; [<br />
                &#8216;type&#8217; =&gt; &#8216;ngram&#8217;<br />
            ],<br />
        ];<br />
        return $tokenizer;<br />
    }<br />
[/code]
</div>
</p></div>
<p>In this case we are increasing the ngram value to <em>3</em>. This will take effect at the next catalog search full text reindex.</p>
<p>To test these customisation values for yourself you can download the full module here <a href="https://github.com/gaiterjones/Magento2_Gaiterjones_Elasticsearch">https://github.com/gaiterjones/Magento2_Gaiterjones_Elasticsearch</a></p>
<p>The module adds a configuration value to <em>Stores -> Configuration -> Catalog -> Search</em> where you can set the minimum score value for Elastic Search results.</p>
<figure id="blog-es_4" aria-describedby="caption-blog-es-4" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_4.jpg" alt="" width="600" height="155" class="aligncenter size-full wp-image-2287" /><figcaption id="caption-blog-es-4" class="wp-caption-text"><br />
Configure search minimum score value in admin<br /></figcaption></figure>
<p>It&#8217;s a real shame that ElasticSearch customisation options such as these are not built into Magento 2 by default to help Merchants improve the search experience. ElasticSearch is new to me, and will be to a lot of merchants and Magento devs upgrading to Magento 2.4. It&#8217;s a very complex system to understand and although we can tweak some values as shown to improve results this is not a great solution to the problem.</p>
<p>If like me you are still not happy with the native Magento 2 ElasticSearch catalog search results the absolute best solution I have found is to migrate to <a href="https://github.com/Smile-SA/elasticsuite" rel="noopener noreferrer" target="_blank">Smile ElasticSuite</a></p>
<p>Simply put, installing the Smile ElasticSuite modules and changing catalog search to ElasticSuite will <strong>immediately</strong> give you almost perfect search results. ElasticSuite is very simple to install and works out of the box improving search results and search relevance.</p>
<h1 id="elasticsuite">Install Smile ElasticSuite</h1>
<p>Here are the steps required to install ElasticSuite.</p>
<p><em>Note that ElasticSuite includes it&#8217;s own custom layered navigation, if you are already using third party layered navigation modules you will need to disable these first before installing elasticsuite.</em></p>
<p>You will need to choose the correct ElasticSuite version for the version of Magento 2 you are using. Here are the options for Magento 2.3.x and 2.4</p>
<ul style="list-style-type:none;">
<li>Magento 2.3.3 -> 2.3.5</li>
<li>
<ul style="list-style-type:none;">
<li>ElasticSuite 2.8.x : <code>composer require smile/elasticsuite ~2.8.0</code></li>
</ul>
</li>
<li>Magento 2.3.5 +</li>
<li>
<ul style="list-style-type:none;">
<li>ElasticSuite 2.9.x : <code>composer require smile/elasticsuite ~2.9.0</code></li>
</ul>
</li>
<li>Magento 2.4.0</li>
<li>
<ul style="list-style-type:none;">
<li>ElasticSuite 2.10.x : <code>composer require smile/elasticsuite ~2.10.0</code></li>
</ul>
</li>
</ul>
<ul>
<p>After installing the modules run <code>setup:upgrade</code> and then configure your ElasticSearch server hostname.</p>
<li><code>bin/magento s:up</code></li>
<li><code>bin/magento config:set -l smile_elasticsuite_core_base_settings/es_client/servers elasticsearch_server_hostname:9200</code></li>
<li><code>bin/magento s:up</code></li>
<li><code>bin/magento index:reindex</code></li>
</ul>
<p>To change your catalog search engine to ElasticSuite navigate to <em>Stores -> Configuration -> Catalog -> Search</em> and select ElasticSuite as the new catalog search engine.</p>
<figure id="blog-es_6-2" aria-describedby="caption-blog-es-6-2" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_6-2.jpg" alt="Configure ElasticSuite as Catalog Search Engine" width="600" height="219" class="size-full wp-image-2304" /><figcaption id="caption-blog-es-6-2" class="wp-caption-text">Configure ElasticSuite as Catalog Search Engine</figcaption></figure>
<p>Refresh caches and the ElasticSuite catalog search engine should now be setup and working &#8211; <em>congratulations</em> &#8211; Magento 2 full text catalog search just got a whole lot better!</p>
<p>if you see the following error in the frontend, simply run the indexer again.</p>
<pre>Exception #0 (LogicException): catalog_product index does not exist yet. Make sure everything is reindexed.</pre>
<p>ElasticSuite has some great features :</p>
<ul>
<li>A new search input form with automatic and relevant search suggestions in a dropdown list</li>
<li>Layered navigation with price sliders and advanced filters</li>
<li>Automatic redirect to product when search returns a single matching product</li>
<li>Automatic spell checked search terms</li>
<li>Smart virtual product categories</li>
<li>Search analysis in admin</li>
</ul>
<p>You will notice straight away when searching for SKUs that ElasticSuite returns more relevant results than native search. </p>
<figure id="attachment_2341" aria-describedby="caption-attachment-2341" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_8.jpg" alt="ElasticSuite sample product SKU search" width="600" height="257" class="size-full wp-image-2341" /><figcaption id="caption-attachment-2341" class="wp-caption-text">ElasticSuite sample product SKU search</figcaption></figure>
<p>Using the SKU search example you can search for all or part of the SKU with or without the hyphen and accurate search results will be returned. Notice below the search for MH03 XL without the hyphen returns the correct results</p>
<figure id="attachment_2343" aria-describedby="caption-attachment-2343" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_9.jpg" alt="ElasticSuite sample product SKU search" width="600" height="332" class="size-full wp-image-2343" /><figcaption id="caption-attachment-2343" class="wp-caption-text">ElasticSuite sample product SKU search</figcaption></figure>
<p>The redirect to product feature when a single matching product is found in search is really useful taking the customer directly to the relevant product.</p>
<p>The search analysis in admin is a great feature allowing you to see how search is being utilised by your customers and which search terms lead to conversions.</p>
<figure id="attachment_2321" aria-describedby="caption-attachment-2321" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/08/blog-es_7.jpg" alt="ElasticSuite search analysis" width="600" height="723" class="size-full wp-image-2321" /><figcaption id="caption-attachment-2321" class="wp-caption-text">ElasticSuite search analysis</figcaption></figure>
<p>For more information on ElasticSuite features and configuration consult the <a href="https://github.com/Smile-SA/elasticsuite/wiki">ElasticSuite Wiki</a> or visit the <a href="https://elasticsuite.io/" rel="noopener noreferrer" target="_blank">website</a>.</p>
<p>Many thanks to the <a href="https://elasticsuite.io/en/#page5" rel="noopener noreferrer" target="_blank">Smile team</a> for making this module freely available to the Magento 2 community.</p>
<h2>Acknowledgements</h2>
<ul>
<li><a target="_blank" href="https://elasticsuite.io/en/" rel="noopener noreferrer">Smile ElasticSuite</a></li>
<li>Moore Digital <a target="_blank" href="https://github.com/mooore-digital/magento2-module-elasticsearch-relevance" rel="noopener noreferrer">Magento 2 Elasticsearch Relevance</a></li>
<li>Sandip &#8211; <a target="_blank" href="https://magento.stackexchange.com/a/307365/7863" rel="noopener noreferrer">ngram plugin</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/how-to-improve-magento-2-elasticsearch-catalog-search-results-and-relevance/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Improve Slow Loading Magento 2 Large Configurable Products</title>
		<link>https://blog.gaiterjones.com/improve-slow-loading-magento-2-large-configurable-products/</link>
					<comments>https://blog.gaiterjones.com/improve-slow-loading-magento-2-large-configurable-products/#comments</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Mon, 29 Jun 2020 17:41:34 +0000</pubDate>
				<category><![CDATA[Catalog]]></category>
		<category><![CDATA[Magento2]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2253</guid>

					<description><![CDATA[Configurable products have changed a lot in Magento 2. Compared to Magento 1, Magento 2 configurable products are just containers for simple products (variations). You can no longer configure pricing...<a class="more-link" href="https://blog.gaiterjones.com/improve-slow-loading-magento-2-large-configurable-products/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<p>Configurable products have changed a lot in Magento 2.</p>
<p>Compared to Magento 1, Magento 2 configurable products are just containers for simple products (variations). You can no longer configure pricing data directly in the configurable product as the parent configurable inherits all it&#8217;s unit, tier price and inventory data from the child simple products. It&#8217;s also much easier to create products with variations such as size, colour etc. and display them in the frontend in various different ways i.e. visual swatches.</p>
<h1>Large Configurable Products are slow to Load!</h1>
<p>One of the downsides to Magento 2 configurable products is that large configurable products can be slow to load in the frontend if a lot of variations are configured. I worked on a store with a product that is available in over 250 colours and four sizes. This resulted in a configurable product with over 1000 child products and whilst theoretically there is no limit to the amount of simple products in a configurable container product in practice, the way Magento 2 builds the frontend product can lead to very slow load times.</p>
<blockquote><p>In the frontend, Magento 2 loads all variations in a giant JSON object and renders that into the DOM. This JSON object is 20 megabytes for 10,000 variations. In the backend, this JSON is also built and passed to a UI component wrapped in XML. PHP’s xmllib is not able to append extremely large XML structures to an existing XML structure.</p></blockquote>
<p>Even with 1000 variations page load time for an uncached configurable product was in excess of 30 seconds.</p>
<h1>Elgentos LCP</h1>
<p>Fortunately the nice people at <a href="https://elgentos.nl/">Elgentos </a>open sourced a <a href="https://github.com/elgentos/LargeConfigProducts">module</a> developed for a customer experiencing exactly this slow loading Magento 2 configurable product problem. <a href="https://github.com/elgentos/LargeConfigProducts"><em>elgentos/LargeConfigProducts</em></a> greatly improves the loading time of large configurable products by pre-caching the product variation data in the backend and loading the frontend variation JSON as an asynchronous ajax request. This results in a much faster load time of the parent product and the cached variation data.</p>
<p>When I tested the module there were some issues with Magento 2.3.x compatibility which the developer had not had time to correct. I made some changes to make it compatible and also added <span>AQMP/RabbitMQ integration. I am using the module in production without any issues and it has made a big difference to page loading times.</span></p>
<p>Here are a few notes on installing and using the module with Magento 2.3.x.</p>
<p>After installing <a href="https://github.com/gaiterjones/LargeConfigProducts"><em>elgentos/LargeConfigProducts</em></a> you should configure the prewarm settings</p>
<figure id="attachment_2254" aria-describedby="caption-attachment-2254" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/06/elgentos_lcp_1.jpg" alt="Elgentos LCP Prewarm Settings" width="600" height="251" class="size-full wp-image-2254" /><figcaption id="caption-attachment-2254" class="wp-caption-text">Elgentos LCP Prewarm Settings</figcaption></figure>
<p>&#8220;Prewarming&#8221; is the process of creating and caching the variation data for configurable products. The module uses Redis to cache the data and you should specify your Redis hostname/IP, TCP port and choose a new database for the data.</p>
<p>The module includes a new indexer that will prewarm all configurable products when you manually reindex with <code>bin/magento index:reindex</code></p>
<p>With the module configured and enabled all configurable products will now load variation data via an Ajax request. If variation product data has not been prewarmed or cached the cache will be updated when the product loads. You can also manually create the product variation data cache using a console command</p>
<p><code>bin/magento lcp:prewarm --products 1234,12345 -force</code></p>
<p>This will force a prewarm of variation data for a configurable product with the ids 1234 and 12345.</p>
<p>When you make a change to a configurable product, or a child of a configurable product the module uses a message queue to update the configurable product cached data. Magento 2.3 has built in <span>AQMP/RabbitMQ integration and you can add a Rabbit MQ server to your Magento 2 system by using the following <code>env.php</code> configuration :</p>
<p><code><br />
    'queue' => [<br />
        'amqp' => [<br />
            'host' => 'RABBITMQ_HOST_NAME',<br />
            'port' => 5672,<br />
            'user' => 'guest',<br />
            'password' => 'guest',<br />
            'virtualhost' => '/'<br />
        ]
    ],<br />
</code></p>
<p>Messages are created by a publisher and actioned by a consumer process in the module. To list all the configured Magento 2 consumer queues use:</p>
<p><code>bin/magento queue:consumers:list</code></p>
<p>You will see that <code>elgentos_magento_lcp_product_prewarm</code> is listed. To run the prewarm consumer use <code>bin/magento queue:consumers:start elgentos_magento_lcp_product_prewarm</code> this will start processing all messages generated by the module and updating the product variation data cache for any products that have been changed.</p>
<p>You should ensure that your consumer process is always running. If you use Docker you can create a small consumer container for this purpose.</p>
<pre class="brush: plain; title: ; notranslate">
    consumer1:
        hostname: shop01_consumer
        build:
            context: ./consumer/
        volumes:
            - &quot;${CONTAINERDATA}/${PROJECT_NAME}/www1/dev/magento2:/var/www/dev/magento2&quot;
        depends_on:
            - mysql
            - rabbitmq
        restart: always
        entrypoint: &#x5B;&quot;php&quot;, &quot;./bin/magento&quot;, &quot;queue:consumers:start&quot;, &quot;elgentos_magento_lcp_product_prewarm&quot;]
</pre>
<p>I can also recommend using the RabbitMQ Docker container <code>image: rabbitmq:management</code> the built in management gui is useful for monitoring message data here you can see the lcp message generation for the prewarm consumer after performing a reindex</p>
<figure id="attachment_2257" aria-describedby="caption-attachment-2257" style="width: 597px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/06/elgentos_lcp_2.jpg" alt="RabbitMQ Management Gui" width="597" height="514" class="wp-image-2257" /><figcaption id="caption-attachment-2257" class="wp-caption-text">RabbitMQ Management Gui</figcaption></figure>
<p>In my opinion this functionality should be built into Magento by default to improve the loading time of large configurable products. Changes might be coming to configurable products in Magento 2.4 so perhaps there will be improvements made in this area.</p>
<p>Many thanks to Elgentos and Peter Jaap Blaakmeer for making this module freely available to the community and allowing me to contribute to it.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/improve-slow-loading-magento-2-large-configurable-products/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title>Magento 2 Custom Dynamic Maintenance Page with NGINX</title>
		<link>https://blog.gaiterjones.com/magento-2-custom-dynamic-maintenance-page-with-nginx/</link>
					<comments>https://blog.gaiterjones.com/magento-2-custom-dynamic-maintenance-page-with-nginx/#respond</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Fri, 29 May 2020 18:54:53 +0000</pubDate>
				<category><![CDATA[Magento2]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2230</guid>

					<description><![CDATA[I was working on my procedures for applying updates to a production Magento 2 site recently and decided it was a pretty good idea to put Magento into maintenance mode...<a class="more-link" href="https://blog.gaiterjones.com/magento-2-custom-dynamic-maintenance-page-with-nginx/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<style>
.embed-container {
	position: relative;
	padding-bottom: 56.25%;
	height: 0;
	overflow: hidden;
	max-width: 100%;
}
.embed-container iframe, .embed-container object, .embed-container embed {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
}
</style>
<p>I was working on my procedures for applying updates to a production Magento 2 site recently and decided it was a pretty good idea to put Magento into maintenance mode first before making any changes or updates that might temporarily break the site and return a nasty error message. The default production maintenance page for Magento 2 looks like this</p>
<figure id="attachment_2233" aria-describedby="caption-attachment-2233" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/maintenance-mode_1.jpg" alt="" width="600" height="194" class="wp-image-2233 size-full" /><figcaption id="caption-attachment-2233" class="wp-caption-text">Default Magento 2 Production Maintenance Page</figcaption></figure>
<p>It&#8217;s not exactly what I would call a thing of beauty. A Google search reveals a plethora of solutions &#8211; but I really wanted something simple. In my mind a custom module with a thousand customisation options for a maintenance page is somewhat overkill. You can also create your own custom response by editing or extending the <code>503.phtml</code> file in <code>pub/errors/default</code>.</p>
<p><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/maintenance-mode_2.jpg" alt="" width="476" height="179" class="aligncenter size-full wp-image-2235" /></p>
<h1 class="title">503 Service Unavailable</h1>
<p>Notice that in production mode the maintenance page returns a 503 error which is correct as we want any visitors (and crawlers) to know that the site is temporarily unavailable. (In development mode this is a much more unfriendlier http 500 error!)</p>
<p>There is however a problem associated with your Magento site returning a 503 error in maintenance mode. If you are using Varnish, and especially if you are using the health probe in varnish the 503 error will cause varnish to eventually announce the server as sick and throw it&#8217;s own extremely unfriendly error &#8211; something about <em>meditating gurus</em>.</p>
<p>If you look at the <a href="https://devdocs.magento.com/guides/v2.3/comp-mgr/trouble/cman/maint-mode.html">Magento docs</a> they actually suggest creating a custom maintenance page via the web server &#8211; Apache or NginX. The examples show a configuration whereby the web server redirects to a custom url when a maintenance file is present on the system.</p>
<pre class="brush: plain; title: ; notranslate">
server {
     listen 80;
     set $MAGE_ROOT /var/www/html/magento2;

     set $maintenance off;

     if (-f $MAGE_ROOT/maintenance.enable) {
         set $maintenance on;
     }

     if ($remote_addr ~ (192.0.2.110|192.0.2.115)) {
         set $maintenance off;
     }

     if ($maintenance = on) {
         return 503;
     }

     location /maintenance {
     }

     error_page 503 @maintenance;

     location @maintenance {
     root $MAGE_ROOT;
     rewrite ^(.*)$ /maintenance.html break;
 }

     include /var/www/html/magento2/nginx.conf;
}
</pre>
<p>Here they are suggesting that if the file <code>maintenance.enable</code> is present NginX will 503 redirect to a maintenance page. A similar config example is available for Apache.</p>
<p>This also works quite well and if you change the file detection to the Magento 2 system generated maintenance file <code>/var/.maintenance.flag</code> As soon as you place Magento into maintenance mode the custom page would be shown &#8211; cool!</p>
<p>But there are still a couple of drawbacks, first with your site returning <em>503</em> for all pages your maintenance page can&#8217;t load any external js or css hosted on your Magento server so your maintenance page needs to be pretty basic. Second you are still returning a <em>503</em> to Varnish which will eventually cause a health error.</p>
<p>Chances are if you are using Varnish you also have an NginX reverse proxy in front of Varnish providing TLS encryption. Or if you are using Docker, NginX is reverse proxying http/s to your containers. If so then this is best place to configure your custom maintenance page and you can create a really nice looking dynamic Magento custom maintenance page that will appear as soon as you place Magento into maintenance mode &#8211; or whenever Magento or Varnish return <em>503</em> errors.</p>
<p>For Docker you will need to mount a volume on NginX giving it access to the <code>var/</code> folder in Magento so that it can detect the <code>.maintenance.flag</code> file.</p>
<p>The NginX config looks like this</p>
<pre class="brush: plain; title: ; notranslate">
    # MAGENTO 2 Maintenance Mode
    set $MAGE2_ROOT /var/www/gaiterjones/magento2/;
    set $maintenance off;
    if (-f $MAGE2_ROOT/.maintenance.flag) {
        set $maintenance on;
    }
    if ($maintenance = on) {
        return 503;
    }
    error_page 503 @maintenance;
    location @maintenance {
        root /var/www/html;
        rewrite ^(.*)$ /magento2-maintenance.html break;
    }
</pre>
<p>Here you can see the Magento var folder is mounted to <code>var/www/gaiterjones/magento2</code> in NginX and if the maintenance file exists we redirect to a local <code>maintenance.html</code> page in <code>var/www/html</code></p>
<p>The custom <code>maintenance.html</code> page can be any kind of page you want, I&#8217;m using a nice responsive page that you can see below.</p>
<p>As soon as you do a <code>bin/magento maintenance:enable</code> NginX will show the maintenance page returning a 503 code to any visiting customers (or search engines). My page refreshes every 30 seconds so as soon as you do <code>bin/magento maintenance:disable</code> customers will automatically see your shop again (hopefully).</p>
<div class="embed-container">
<iframe src="/dropbox/magento2/magento2-maintenance.html"></iframe><br />
<span>Magento 2 Maintenance Page</span>
</div>
<p>Responsive HTML template by <a target="_blank" href="https://html5up.net/" rel="noopener noreferrer">HTML5UP</a> download this template <a target="_blank" href="/dropbox/magento2/magento2-maintenance.html" rel="noopener noreferrer">here</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/magento-2-custom-dynamic-maintenance-page-with-nginx/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Magento 2 Javascript Bundling with Magepack</title>
		<link>https://blog.gaiterjones.com/magento-2-javascript-bundling-with-magepack/</link>
					<comments>https://blog.gaiterjones.com/magento-2-javascript-bundling-with-magepack/#comments</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Sun, 03 May 2020 19:51:28 +0000</pubDate>
				<category><![CDATA[Javascript Bundling]]></category>
		<category><![CDATA[Magento2]]></category>
		<category><![CDATA[bundling]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[lighthouse]]></category>
		<category><![CDATA[magento2]]></category>
		<category><![CDATA[magepack]]></category>
		<category><![CDATA[pagespeed]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2171</guid>

					<description><![CDATA[Magento 2 PageSpeed (Lighthouse) performance audit results for mobile and desktop are notoriously bad. Imagine you have worked for months on a new Magento 2 eCommerce store, followed best practices...<a class="more-link" href="https://blog.gaiterjones.com/magento-2-javascript-bundling-with-magepack/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<p>Magento 2 PageSpeed (<a href="https://developers.google.com/web/tools/lighthouse">Lighthouse</a>) performance audit results for mobile and desktop are notoriously bad. Imagine you have worked for months on a new Magento 2 eCommerce store, followed best practices for setup and optimisation, the store seems to be running fine but the first time you run a Lighthouse report you see a performance score like this:<img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-pagespeed_1.jpg" alt="" width="250" height="250" class="aligncenter size-full wp-image-2172" />There are a lot of factors that can affect the Lighthouse performance results for any website but for Magento 2 a big performance killer is the sheer amount of external resources required to render a page whether it be a product page, cms page or category page. Some of these render blocking resources such as Javascript or CSS can cause significant delays in page loading and affect performance. You will see this type of performance problem identified in Lighthouse as &#8220;Eliminate render-blocking resources&#8221;.</p>
<p>Magento 2 uses the RequireJs <span>javascript module system to load Javascript source code <em>required</em> for each Magento 2 page. If you have a lot of custom features with modules implementing additional Magento 2 Javascript<em> mixins </em>the number of Javascript resources in addition to the core javascript code required by Magento will increase and adversely affect page loading performance. As an example, here is the network console log from a really simple product page from my development site, you can see that there are 194 requests for Javascript resources!</span></p>
<figure id="attachment_2196" aria-describedby="caption-attachment-2196" style="width: 473px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-pagespeed_8.png" alt="Simple Magento 2 product page loads 194 Javascript files" width="473" height="361" class="wp-image-2196" /><figcaption id="caption-attachment-2196" class="wp-caption-text">Simple Magento 2 product page loads 194 Javascript files</figcaption></figure>
<p>There are various ways to try and reduce the performance impact of loading lots of Javascript including using http2 which is great at handling small file requests quickly or minifying the Javascript source to reduce it&#8217;s size but the most effective way of optimising Javascript loading is to use bundling.</p>
<p style="text-align: center;"><em>Javascript bundling is a technique that combines or bundles multiple files in order to reduce the number of HTTP requests that are required to load a page.<br />
</em></p>
<p>Magento 2 has a built in javascript bundler that is extremely ineffective! Users report it creating a <a href="https://github.com/magento/magento2/issues/4506">huge multi megabyte javascript</a> file that decreases performance instead of improving it. You will actually see the recommendation <em>not</em> to use the built in Magento 2 bundling referenced in Lighthouse reports &#8211; &#8220;Disable Magento&#8217;s built-in <a rel="noopener noreferrer" target="_blank" href="https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/themes/js-bundling.html">JavaScript bundling and minification</a>, and consider using <a rel="noopener noreferrer" target="_blank" href="https://github.com/magento/baler/">baler</a> instead.&#8221;</p>
<p><a href="https://github.com/magento/baler/">Baler</a> mentioned here is an AMD (Asynchronous Module Definition) module bundler / preloader for Magento 2 stores. You will find a lot of Magento 2 js bundling guides that recommend using Baler but for the average developer (like me) or Magento 2 merchant the bundling process with Baler can be quite complex and daunting. There is however a new Magento 2 js bundler available that is much easier to use.</p>
<h1>MageSuite Magepack</h1>
<p>The <a href="https://github.com/magesuite/magepack">Magepack</a> from MageSuite is a &#8220;<em>Next generation Magento 2 advanced JavaScript bundler</em>&#8221; it&#8217;s pretty easy to implement and as of version 2.0 the results it achieves are very impressive.</p>
<ul>
<li>Up to 91 points mobile score in Google Lighthouse.</li>
<li>Up to 98% reduction in JavaScript file requests.</li>
<li>Up to 44% reduction in transferred JavaScript size.</li>
<li>Up to 75% reduction in total load time.</li>
<li>Works with Magento&#8217;s JavaScript minification and merging enabled.</li>
<li>Uses custom solution (inspired by Baler)</li>
</ul>
<p><span>I installed Magepack on my Magento 2 development site in May 2020 and achieved a 100 desktop performance score with PageSpeed  &#8211;</span></p>
<p><a href="https://developers.google.com/speed/pagespeed/insights/?url=http%3A%2F%2Fmagento2.gaiterjones.com%2Fen%2Faffirm-water-bottle.html&amp;tab=desktop"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-pagespeed_2.jpg" alt="" width="600" height="209" class="aligncenter wp-image-2175 size-full" /></a>This is a simple product page, using the default Luma theme and I am also using Nginx as a container proxy running the PageSpeed module, so you probably won&#8217;t achieve this kind of result on a real world product page but you will see a <strong>huge</strong> improvement. Check the results yourself <a href="https://developers.google.com/speed/pagespeed/insights/?url=http%3A%2F%2Fmagento2.gaiterjones.com%2Fen%2Faffirm-water-bottle.html&amp;tab=desktop">here</a>.</p>
<p>Let&#8217;s look at how to setup and install MagePack for Magento 2.3.x / 2.4.x.</p>
<h2>Setup and install Magepack for Magento 2.3.x and 2.4.x</h2>
<p>MagePack consists of a NodeJS bundler app and a Magento 2 module. The bundler app runs on Node JS v10 or higher. I&#8217;m running MagePack in my Docker Magento 2 php container, it&#8217;s running Ubuntu server 20.04LTS and I&#8217;ve tested Magepack with Magento 2.3.3, 2.3.5 and 2.4.1. To install Node JS simply run</p>
<pre class="brush: bash; title: ; notranslate">
curl -sL https://deb.nodesource.com/setup_10.x | bash -
apt-get install -y nodejs
</pre>
<p>Ubuntu will probably need some more dependencies before MagePack will install</p>
<pre class="brush: bash; title: ; notranslate">apt-get install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget</pre>
<p>Finally to install the NodeJS MagePack app itself run<br />
<code>npm install -g magepack  --unsafe-perm=true --allow-root</code></p>
<figure id="attachment_2180" aria-describedby="caption-attachment-2180" style="width: 626px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-magepack-screenshot.jpg" alt="Installing Magepack NodeJS app and dependencies" width="626" height="300" class="wp-image-2180" /><figcaption id="caption-attachment-2180" class="wp-caption-text">Installing Magepack NodeJS app and dependencies</figcaption></figure>
<p>You will see that Magepack pulls down Chromium &#8211; it needs a web browser to analyse your Magento 2 site, most of the dependencies installed earlier are required for Chromium.</p>
<p>With Magepack installed, we now need to install the <a href="https://github.com/magesuite/magepack-magento">Magepack Magento 2 module</a></p>
<p><code>composer require creativestyle/magesuite-magepack</code></p>
<p>Next, depending on the version of Magento 2 you are running you might need to install some patches.</p>
<ul>
<li>For Magento 2.3.3 and earlier 7 patches are required</li>
<li>For Magento 2.3.4 and 2.3.5 1 patch is required</li>
<li><strong>For Magento 2.4.1 no patches are required (tested November 2020)</strong></li>
</ul>
<p>The most painless way of patching Magento 2 is to use <a href="https://github.com/cweagans/composer-patches">Cweagans/Composer-Patches</a></p>
<p><code>composer require cweagans/composer-patches</code></p>
<p>You will find all the patches you need here : https://github.com/integer-net/magento2-requirejs-bundling</p>
<p>In your Magento 2 installation folder create a <em>patches</em> folder copy the patches into it and edit your Magento 2 composer.json file to include the following composer extra patches config.</p>
<p>If you see the error <code>Evaluation failed: ReferenceError: BASE_URL is not defined</code> running magepack generate with 2.3.5+ or 2.4.x check your CSP configuration, or try disabling the CSP module temporarily &#8211; errors reported by CSP may create a magepack error.</p>
<span class="collapseomatic collapse" id="id680b6e88cf9d9"  tabindex="0" title="2.3.3 composer extra patches config"    >2.3.3 composer extra patches config</span><div id="target-id680b6e88cf9d9" class="collapseomatic_content ">
[text]
    &quot;extra&quot;: {<br />
            &quot;magento-force&quot;: &quot;override&quot;,<br />
            &quot;composer-exit-on-patch-failure&quot;: true,<br />
            &quot;patches&quot;: {<br />
                &quot;magento/magento2-base&quot;: {<br />
                    &quot;[Performance] Fix missing shims and phtml files with mage-init directives (https://github.com/magento/magento2/commit/db43c11c6830465b764ede32abb7262258e5f574)&quot;: &quot;patches/composer/M233/github-pr-4721-base.diff&quot;,<br />
                    &quot;Refactor JavaScript mixins module https://github.com/magento/magento2/pull/25587&quot;: &quot;patches/composer/M233/github-pr-25587-base.diff&quot;<br />
                },<br />
                &quot;magento/module-braintree&quot;: {<br />
                    &quot;[Performance] Fix missing shims and phtml files with mage-init directives (https://github.com/magento/magento2/commit/db43c11c6830465b764ede32abb7262258e5f574)&quot;: &quot;patches/composer/M233/github-pr-4721-braintree.diff&quot;<br />
                },<br />
                &quot;magento/module-catalog&quot;: {<br />
                    &quot;[Performance] Fix missing shims and phtml files with mage-init directives (https://github.com/magento/magento2/commit/db43c11c6830465b764ede32abb7262258e5f574)&quot;: &quot;patches/composer/M233/github-pr-4721-catalog.diff&quot;<br />
                },<br />
                &quot;magento/module-customer&quot;: {<br />
                    &quot;[Performance] Fix missing shims and phtml files with mage-init directives (https://github.com/magento/magento2/commit/db43c11c6830465b764ede32abb7262258e5f574)&quot;: &quot;patches/composer/M233/github-pr-4721-customer.diff&quot;<br />
                },<br />
                &quot;magento/module-msrp&quot;: {<br />
                    &quot;[Performance] Fix missing shims and phtml files with mage-init directives (https://github.com/magento/magento2/commit/db43c11c6830465b764ede32abb7262258e5f574)&quot;: &quot;patches/composer/M233/github-pr-4721-msrp.diff&quot;<br />
                },<br />
                &quot;magento/module-paypal&quot;: {<br />
                    &quot;[Performance] Fix missing shims and phtml files with mage-init directives (https://github.com/magento/magento2/commit/db43c11c6830465b764ede32abb7262258e5f574)&quot;: &quot;patches/composer/M233/github-pr-4721-paypal.diff&quot;<br />
                },<br />
                &quot;magento/module-theme&quot;: {<br />
                    &quot;[Performance] Fix missing shims and phtml files with mage-init directives (https://github.com/magento/magento2/commit/db43c11c6830465b764ede32abb7262258e5f574)&quot;: &quot;patches/composer/M233/github-pr-4721-theme.diff&quot;,<br />
                    &quot;fix_baler_jquery_cookie&quot;: &quot;https://gist.github.com/tdgroot/f95c398c565d9bbb83e0a650cdf67617/raw/69ee2d001ff509d25d1875743e417d914e20fd85/fix_baler_jquery_cookie.patch&quot;<br />
                }<br />
            }<br />
        },<br />
[/text]
</div>
<span class="collapseomatic collapse" id="id680b6e88cfa10"  tabindex="0" title="2.3.4, 2.3.5 composer extra patches config"    >2.3.4, 2.3.5 composer extra patches config</span><div id="target-id680b6e88cfa10" class="collapseomatic_content ">
[text]
    &quot;extra&quot;: {<br />
            &quot;magento-force&quot;: &quot;override&quot;,<br />
            &quot;composer-exit-on-patch-failure&quot;: true,<br />
            &quot;patches&quot;: {<br />
                &quot;magento/magento2-base&quot;: {<br />
                    &quot;Refactor JavaScript mixins module https://github.com/magento/magento2/pull/25587&quot;: &quot;patches/composer/M234/github-pr-25587-base.diff&quot;<br />
                }<br />
            }<br />
        },<br />
[/text]
</div>
<p>Now run <code>composer update</code> Magento 2 will be patched and we are good to go.</p>
<h2>Let’s get ready to bundle</h2>
<p>Magepack needs to analyse pages from your Magento 2 store to determine the Javascript files your store is using and how they can be bundled. It saves this information in a configuration file called <em>magepack.config.js</em>. The magepack config file is generated by analysing three different type of pages from your Magento 2 store, a cms page i.e. the home page, a category page and a product page. This is done using the <code>magepack generate</code> command and supplying three store urls.</p>
<div>
<pre class="brush: bash; title: ; notranslate">
magepack generate --cms-url=&quot;http://magento2.gaiterjones.com/&quot; --category-url=&quot;http://magento2.gaiterjones.com/en/buy-x-get-y.html&quot; --product-url=&quot;http://magento2.gaiterjones.com/en/affirm-water-bottle.html&quot;
</pre>
<p>Run this command in the root folder of your Magento 2 installation to create the magepack.config.js file. It&#8217;s worth noting that you could run this generate command from any system, and just copy the generated config file to your Magento 2 server.</p>
<p>If you take a look at magepack.config.js you will see it contains references to all the javascript required to load Magento pages. Below is an example from a product page.</p>
<span class="collapseomatic collapse" id="id680b6e88cfa38"  tabindex="0" title="example product section from magepack.config.js"    >example product section from magepack.config.js</span><div id="target-id680b6e88cfa38" class="collapseomatic_content ">
[code language=&#8221;javascript&#8221;]
    name: &#8216;product&#8217;,<br />
    modules: {<br />
      &#8216;Magento_Catalog/js/price-utils&#8217;: &#8216;Magento_Catalog/js/price-utils&#8217;,<br />
      &#8216;Magento_Catalog/js/price-box&#8217;: &#8216;Magento_Catalog/js/price-box&#8217;,<br />
      &#8216;Magento_Wishlist/js/add-to-wishlist&#8217;: &#8216;Magento_Wishlist/js/add-to-wishlist&#8217;,<br />
      &#8216;Magento_Cookie/js/require-cookie&#8217;: &#8216;Magento_Cookie/js/require-cookie&#8217;,<br />
      &#8216;Magento_Swatches/js/configurable-customer-data&#8217;: &#8216;Magento_Swatches/js/configurable-customer-data&#8217;,<br />
      &#8216;Magento_Review/js/error-placement&#8217;: &#8216;Magento_Review/js/error-placement&#8217;,<br />
      &#8216;Magento_Review/js/process-reviews&#8217;: &#8216;Magento_Review/js/process-reviews&#8217;,<br />
      &#8216;Elgentos_LargeConfigProducts/js/swatch-renderer-mixin&#8217;: &#8216;Elgentos_LargeConfigProducts/js/swatch-renderer-mixin&#8217;,<br />
      &#8216;text!Magento_Theme/templates/breadcrumbs.html&#8217;: &#8216;Magento_Theme/templates/breadcrumbs.html&#8217;,<br />
      &#8216;magnifier/magnifier&#8217;: &#8216;magnifier/magnifier&#8217;,<br />
      &#8216;magnifier/magnify&#8217;: &#8216;magnifier/magnify&#8217;,<br />
      &#8216;Magento_Catalog/js/gallery&#8217;: &#8216;Magento_Catalog/js/gallery&#8217;,<br />
      &#8216;Magento_ProductVideo/js/load-player&#8217;: &#8216;Magento_ProductVideo/js/load-player&#8217;,<br />
      &#8216;Magento_ProductVideo/js/fotorama-add-video-events&#8217;: &#8216;Magento_ProductVideo/js/fotorama-add-video-events&#8217;,<br />
      &#8216;Magento_Theme/js/model/breadcrumb-list&#8217;: &#8216;Magento_Theme/js/model/breadcrumb-list&#8217;,<br />
      &#8216;Magento_Theme/js/view/breadcrumbs&#8217;: &#8216;Magento_Theme/js/view/breadcrumbs&#8217;,<br />
      &#8216;Magento_Theme/js/view/add-home-breadcrumb&#8217;: &#8216;Magento_Theme/js/view/add-home-breadcrumb&#8217;,<br />
      &#8216;Magento_Catalog/js/product/breadcrumbs&#8217;: &#8216;Magento_Catalog/js/product/breadcrumbs&#8217;,<br />
      &#8216;jquery/jquery.parsequery&#8217;: &#8216;jquery/jquery.parsequery&#8217;,<br />
      &#8216;Magento_ConfigurableProduct/js/options-updater&#8217;: &#8216;Magento_ConfigurableProduct/js/options-updater&#8217;,<br />
      &#8216;Magento_Review/js/validate-review&#8217;: &#8216;Magento_Review/js/validate-review&#8217;,<br />
      &#8216;Magento_Swatches/js/swatch-renderer&#8217;: &#8216;Magento_Swatches/js/swatch-renderer&#8217;,<br />
      &#8216;Magento_Catalog/product/view/validation&#8217;: &#8216;Magento_Catalog/product/view/validation&#8217;,<br />
      &#8216;Magento_Catalog/js/product/view/product-ids&#8217;: &#8216;Magento_Catalog/js/product/view/product-ids&#8217;,<br />
      &#8216;Magento_Catalog/js/product/view/product-ids-resolver&#8217;: &#8216;Magento_Catalog/js/product/view/product-ids-resolver&#8217;,<br />
      &#8216;Magento_Catalog/js/catalog-add-to-cart&#8217;: &#8216;Magento_Catalog/js/catalog-add-to-cart&#8217;,<br />
      &#8216;Magento_Catalog/js/validate-product&#8217;: &#8216;Magento_Catalog/js/validate-product&#8217;,<br />
      &#8216;Magento_Catalog/js/product/view/provider&#8217;: &#8216;Magento_Catalog/js/product/view/provider&#8217;,<br />
      &#8216;text!mage/gallery/gallery.html&#8217;: &#8216;mage/gallery/gallery.html&#8217;,<br />
      &#8216;text!Magento_InstantPurchase/template/confirmation.html&#8217;: &#8216;Magento_InstantPurchase/template/confirmation.html&#8217;,<br />
      &#8216;Magento_InstantPurchase/js/view/instant-purchase&#8217;: &#8216;Magento_InstantPurchase/js/view/instant-purchase&#8217;,<br />
      &#8216;Magento_Review/js/view/review&#8217;: &#8216;Magento_Review/js/view/review&#8217;,<br />
      &#8216;fotorama/fotorama&#8217;: &#8216;fotorama/fotorama&#8217;,<br />
      &#8216;mage/gallery/gallery&#8217;: &#8216;mage/gallery/gallery&#8217;,<br />
      &#8216;text!Magento_InstantPurchase/template/instant-purchase.html&#8217;: &#8216;Magento_InstantPurchase/template/instant-purchase.html&#8217;<br />
    }<br />
  },<br />
[/code]
</div>
<p>All that remains now is for us to create the bundle files and deploy them for all our store views and themes. This is simply done with the <code>magepack bundle</code> command which you can execute from the Magento installation root folder. If you are running in development mode, deploy frontend static files first.</p>
<p><code>magepack bundle</code></p>
<figure id="attachment_2194" aria-describedby="caption-attachment-2194" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-pagespeed_5.jpg" alt="Magepacl bundle command" width="600" height="231" class="wp-image-2194 size-full" /><figcaption id="caption-attachment-2194" class="wp-caption-text">Magepack bundle command</figcaption></figure>
<p>Finally enable Magepack Javascript bundling in admin :</p>
<p style="text-align: center;"><strong>Stores &#8211; Configuration &#8211; Advanced &#8211; Developer &#8211; Javascript Settings</strong></p>
<figure id="attachment_2193" aria-describedby="caption-attachment-2193" style="width: 600px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-pagespeed_4.jpg" alt="Enable Magepack bundling in admin" width="600" height="201" class="wp-image-2193 size-full" /><figcaption id="caption-attachment-2193" class="wp-caption-text">Enable Magepack bundling in admin</figcaption></figure>
<p>If you are in production mode these options will be hidden in admin. To enable Magepack Javascript bundling from the command line use</p>
<p><code>bin/magento config:set dev/js/enable_magepack_js_bundling 1</code></p>
<p>Note that you should also enable the other Javascript optimisation options here including minfy javascript files and move js code to the bottom of the page &#8211; but <strong>don&#8217;t</strong> enable the default bundling!</p>
<p>MagePack Javascript bundling should now be enabled. To check it&#8217;s working go to a Magento 2 product page and look at the source code, do a search for &#8220;bundle&#8221; and you should see the magepack javascript bundles</p>
<p><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-pagespeed_6.jpg" alt="" width="1003" height="158" class="aligncenter wp-image-2195" /></p>
<p>Now refresh the page and have a look at your network log</p>
<figure id="attachment_2197" aria-describedby="caption-attachment-2197" style="width: 447px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-pagespeed_9.png" alt="After bundling there are only 7 js requests on the product page" width="447" height="269" class="wp-image-2197" /><figcaption id="caption-attachment-2197" class="wp-caption-text">After bundling there are only 7 js requests on the product page</figcaption></figure>
<p>Instead of loading 194 Javascript files, the product page now loads 7, Magepack has bundled all the Javascript into two main bundle files.</p>
<p>I guess it&#8217;s now time to look at the PageSpeed Lighthouse performance reports for your optimised Magento 2 pages. If you are using the Chrome browser simply run a Lighthouse report from the DevTools page. You can also use Googles PageSpeed insights tool at <a href="https://developers.google.com/speed/pagespeed/insights/">https://developers.google.com/speed/pagespeed/insights/</a></p>
<p>This is the improvement I saw in a live production Magento 2 site</p>
<figure id="attachment_2224" aria-describedby="caption-attachment-2224" style="width: 800px" class="wp-caption aligncenter"><img loading="lazy" decoding="async" src="https://blog.gaiterjones.com/wp-content/uploads/2020/05/blog-pagespeed_12.jpg" alt="Performance results before and after bundling" width="800" height="350" class="wp-image-2224 size-full" /><figcaption id="caption-attachment-2224" class="wp-caption-text">Performance results before and after bundling</figcaption></figure>
<p>If you don’t see a big improvement remember there are a lot of other factors taken into Lighthouse performance reports. Work through the report and try to find out where you can make further improvements.</p>
<h2>Deployment in production</h2>
<p>Whenever you flush your sites static files you will need to remember to run <code>magepack bundle</code> again. In production mode you should add this to your deployment process</p>
<pre class="brush: bash; title: ; notranslate">
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy en_GB --area adminhtml
bin/magento setup:static-content:deploy en_GB --area frontend --theme MY/Theme -f
magepack bundle
bin/magento cache:clean
</pre>
<p><strong>Top Tip</strong> &#8211; if for any reason you want to generate the magepack.config.js bundle config again remember to disable the Magepack module first!</p>
<h2>Testing and Troubleshooting</h2>
<p>You should test your store thoroughly to make sure there are no Javascript problems caused by the bundling process. Magepack cannot always 100% bundle all the Javascript required by some pages. Check your web browser console for errors. If you find some features of your store are not working, try and identify if the code was included in the magepack.config.js file. Try removing the code from the bundle and test again.</p>
<h2>Selective Bundling</h2>
<p>If you do not want to bundle at checkout, or any other specific pages take a look at the isEnabled method in the Block\BundlesLoader class. This method determines whether the magepack module is enabled and if bundling should be activated. Simply detect any page here i.e. checkout pages and return false to disable bundling at checkout.</p>
<pre class="brush: php; title: ; notranslate">

        \Magento\Framework\App\Request\Http $request,
        ....
        $this-&gt;request = $request;
        ....
public function isEnabled()
{
        if ($this-&gt;request-&gt;getFullActionName() == 'checkout_index_index') {

            // disable for checkout
            //
            return false;
        }

...


</pre>
<h2>Magepack for Magento Cloud</h2>
<p>You need to have magepack.config in repo, add this to magento.app.yaml:</p>
<pre class="brush: bash; title: ; notranslate">
echo &quot;\n================================== Install and configure Magepack - Start ===============================&quot;
unset NPM_CONFIG_PREFIX
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.35.1/install.sh | bash
export NVM_DIR=&quot;$HOME/.nvm&quot;
&#x5B; -s &quot;$NVM_DIR/nvm.sh&quot;  ] &amp;&amp; \. &quot;$NVM_DIR/nvm.sh&quot;
nvm install --lts=dubnium
npm install -g magepack
echo &quot;\n================================== create bundle ===============================&quot;
magepack bundle
echo &quot;\n================================== Install and configure Magepack - END =================================&quot;
</pre>
<p><em>Thanks to Solteq for this information.</em></p>
<p>Magepack is pretty new with updates being made regularly, be sure to check out the projects GitHub page for new issues.</p>
<h3>References</h3>
<p><a href="https://github.com/magesuite/magepack">https://github.com/magesuite/magepack</a><br />
<a href="https://github.com/magesuite/magepack-magento">https://github.com/magesuite/magepack-magento</a><br />
<a href="https://www.integer-net.com/magento-2-javascript-bundling-integer-net-2/">https://www.integer-net.com/magento-2-javascript-bundling-integer-net-2/</a><br />
<a href="https://gist.github.com/tdgroot/f95c398c565d9bbb83e0a650cdf67617">https://gist.github.com/tdgroot/f95c398c565d9bbb83e0a650cdf67617</a><br />
<a href="https://github.com/integer-net/magento2-requirejs-bundling">https://github.com/integer-net/magento2-requirejs-bundling</a><br />
<a href="https://developers.google.com/speed/pagespeed/module">https://developers.google.com/speed/pagespeed/module</a></p>
<p><a href="https://blog.gaiterjones.com/magento-google-pagespeed-jscsshtmlminify-optimisation/">PageSpeed optimisation in Magento 1</a></p>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/magento-2-javascript-bundling-with-magepack/feed/</wfw:commentRss>
			<slash:comments>37</slash:comments>
		
		
			</item>
		<item>
		<title>GMAIL TLS Negotiation failed, the certificate doesn&#8217;t match the host</title>
		<link>https://blog.gaiterjones.com/gmail-tls-negotiation-failed-the-certificate-doesnt-match-the-host/</link>
					<comments>https://blog.gaiterjones.com/gmail-tls-negotiation-failed-the-certificate-doesnt-match-the-host/#respond</comments>
		
		<dc:creator><![CDATA[PAJ]]></dc:creator>
		<pubDate>Thu, 16 Apr 2020 15:46:43 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[Encryption]]></category>
		<category><![CDATA[Google Gmail]]></category>
		<guid isPermaLink="false">https://blog.gaiterjones.com/?p=2152</guid>

					<description><![CDATA[Gmail lets you Send emails from a different email address or alias so that If you use other email addresses or providers, you can send email from that address via...<a class="more-link" href="https://blog.gaiterjones.com/gmail-tls-negotiation-failed-the-certificate-doesnt-match-the-host/" title="Continue reading">Continue reading</a>]]></description>
										<content:encoded><![CDATA[<p>Gmail lets you Send emails from a different email address or alias so that If you use other email addresses or providers, you can send email from that address via Gmail.</p>
<p>To do this you configure an alias account in Gmail settings configured with the credentials of your mail service. This is useful if you want to consolidate email accounts or if you have a private email server and want to use Gmail to send email via this server using your various private email address domains.</p>
<p>At the start of April 2020 Google rolled out a security update that affected mail delivery using third party accounts. All emails sent via the alias account would not deliver, bouncing back with the error <em>TLS Negotiation failed, the certificate doesn&#8217;t match the host.</em></p>
<h1>TLS Negotiation failed, the certificate doesn&#8217;t match the host.</h1>
<p>Google appears to be enforcing a new email encryption policy for secureTLS connections including validating that the host name on the mail server TLS certificate matches the canonical hostname (MX record) of the third party mail account.</p>
<p>If the host name does not match you can no longer use an encrypted TLS connection in Gmail to send email via your (or your ISP&#8217;s) mail servers.</p>
<p>For example, if your MX record resolves to <em>mail.domain.com</em> but the TLS certificate presented is for <em>smtp.domain.com</em> then Gmail will not connect to your mail server. For some users the only option to get mail working again is to revert to an unencrypted connection &#8211; strange that Google even allow that!</p>
<p><span>Google have updated the help article for </span><a href="https://support.google.com/mail/answer/22370" rel="nofollow noopener noreferrer" target="_blank">Send emails from a different address or alias</a><span> to include a section about this problem.</span></p>
<p><strong>Google also no longer accept self signed certificates for TLS mail connections.</strong></p>
<p>I use an EXIM4 docker container for my private mail relay, and use Gmail as the hub for my email send/receive. To workaround this problem I created a docker Certbot container and issued new LetsEncrypt TLS certificates for all my private mail domains used with Gmail as well as the primary TLS certificate for my Mail server.</p>
<p>I can confirm this resolves the problem and third party provider email sending via Gmail is now working again.</p>
<p>For anyone using Exim4 the way to configure Exim to use multiple TLS certificates is to dynamically match them to your mail domain, I did this using</p>
<p>tls_privatekey=${if exists{/etc/exim4/tls/exim.key.${tls_sni}}{/etc/exim4/tls/exim.key.${tls_sni}}{/etc/exim4/tls/exim.key.mail.defaultdomain.com}}<br />
tls_certificate=${if exists{/etc/exim4/tls/exim.crt.${tls_sni}}{/etc/exim4/tls/exim.crt.${tls_sni}}{/etc/exim4/tls/exim.crt.mail.defaultdomain.com}}</p>
<p><a href="https://support.google.com/mail/thread/38336515?hl=en">Thousands of people</a> have been affected by this. Considering the amount of people working from home or struggling to work at all during the Corona Virus pandemic its really bad timing by Google to implement new email security policies that are service affecting for a lot of users.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.gaiterjones.com/gmail-tls-negotiation-failed-the-certificate-doesnt-match-the-host/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
