<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://kezhan.info/atom.xml" rel="self" type="application/atom+xml" /><link href="https://kezhan.info/" rel="alternate" type="text/html" /><updated>2024-11-23T14:12:28+00:00</updated><id>https://kezhan.info/atom.xml</id><title type="html">KB</title><entry><title type="html">How to get CART of a session in Sitecore CDP &amp;amp; Personalize</title><link href="https://kezhan.info/2022/11/23/How-to-get-CART-of-a-session-in-Sitecore-CDP-Personalize.html" rel="alternate" type="text/html" title="How to get CART of a session in Sitecore CDP &amp;amp; Personalize" /><published>2022-11-23T00:00:00+00:00</published><updated>2022-12-02T00:00:00+00:00</updated><id>https://kezhan.info/2022/11/23/How-to-get-CART-of-a-session-in-Sitecore-CDP--Personalize</id><content type="html" xml:base="https://kezhan.info/2022/11/23/How-to-get-CART-of-a-session-in-Sitecore-CDP-Personalize.html"><![CDATA[<div>In order to get cart information such as total cart price, items in the cart etc.. in a guest session in Sitecore CDP &amp; Personalize, there is no cart object to use directly. Instead, ADD events in the session need to be aggregated until a CLEAR_CART event is reached. And note that for multiple ADD events with the same Product item id, only the first one is counted.</div>
<div><br /></div>
<div>Below is the code example that can be used in Audience Filters or Decision Models to get the total cart price.</div>
<div><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>if (entity &amp;&amp; guest &amp;&amp; guest.sessions) { </div><div>&nbsp; &nbsp; var currentWebSession = null; </div><div>&nbsp; &nbsp; // Find current session&nbsp;  </div><div>&nbsp; &nbsp; guest.sessions.forEach((session) =&gt; { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; if (session.sessionType === 'WEB' &amp;&amp; session.ref == entity.sessionRef) { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; currentWebSession = session; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; } </div><div>&nbsp; &nbsp; }); </div><div>&nbsp; &nbsp; if (currentWebSession !== null) { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; // Get cart price  </div><div>&nbsp; &nbsp; &nbsp; &nbsp; var cartPrice = 0; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; var itemIds = []; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; for (var i = 0; i &lt; currentWebSession.events.length; i++) { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var e = currentWebSession.events[i]; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (e.type === "CLEAR_CART") { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (e.type === "ADD" &amp;&amp; e.arbitraryData &amp;&amp; e.arbitraryData.product &amp;&amp; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; e.arbitraryData.product.item_id &amp;&amp; e.arbitraryData.product.price &amp;&amp; e.arbitraryData.product.quantity) { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (itemIds.indexOf(e.arbitraryData.product.item_id) == -1) { // Only the first ADD event of the same product item id is counted </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cartPrice = cartPrice + (e.arbitraryData.product.price * e.arbitraryData.product.quantity); </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; itemIds.push(e.arbitraryData.product.item_id); </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div><div>&nbsp; &nbsp; &nbsp; &nbsp; } </div><div>&nbsp; &nbsp; } </div><div>}</div></div>
<div>Note: entity is normally the object that have the trigger entity information such as when triggered by a custom event etc...</div>]]></content><author><name></name></author><summary type="html"><![CDATA[In order to get cart information such as total cart price, items in the cart etc.. in a guest session in Sitecore CDP &amp; Personalize, there is no cart object to use directly. Instead, ADD events in the session need to be aggregated until a CLEAR_CART event is reached. And note that for multiple ADD events with the same Product item id, only the first one is counted. Below is the code example that can be used in Audience Filters or Decision Models to get the total cart price. if (entity &amp;&amp; guest &amp;&amp; guest.sessions) { &nbsp; &nbsp; var currentWebSession = null; &nbsp; &nbsp; // Find current session&nbsp; &nbsp; &nbsp; guest.sessions.forEach((session) =&gt; { &nbsp; &nbsp; &nbsp; &nbsp; if (session.sessionType === 'WEB' &amp;&amp; session.ref == entity.sessionRef) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; currentWebSession = session; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; }); &nbsp; &nbsp; if (currentWebSession !== null) { &nbsp; &nbsp; &nbsp; &nbsp; // Get cart price &nbsp; &nbsp; &nbsp; &nbsp; var cartPrice = 0; &nbsp; &nbsp; &nbsp; &nbsp; var itemIds = []; &nbsp; &nbsp; &nbsp; &nbsp; for (var i = 0; i &lt; currentWebSession.events.length; i++) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var e = currentWebSession.events[i]; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (e.type === "CLEAR_CART") { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (e.type === "ADD" &amp;&amp; e.arbitraryData &amp;&amp; e.arbitraryData.product &amp;&amp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; e.arbitraryData.product.item_id &amp;&amp; e.arbitraryData.product.price &amp;&amp; e.arbitraryData.product.quantity) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (itemIds.indexOf(e.arbitraryData.product.item_id) == -1) { // Only the first ADD event of the same product item id is counted &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cartPrice = cartPrice + (e.arbitraryData.product.price * e.arbitraryData.product.quantity); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; itemIds.push(e.arbitraryData.product.item_id); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; } } Note: entity is normally the object that have the trigger entity information such as when triggered by a custom event etc...]]></summary></entry><entry><title type="html">403 forbidden errors when deploying JSS apps to Sitecore</title><link href="https://kezhan.info/2022/11/19/403-forbidden-errors-when-deploying-JSS-apps-to-Sitecore.html" rel="alternate" type="text/html" title="403 forbidden errors when deploying JSS apps to Sitecore" /><published>2022-11-19T00:00:00+00:00</published><updated>2022-11-19T00:00:00+00:00</updated><id>https://kezhan.info/2022/11/19/403-forbidden-errors-when-deploying-JSS-apps-to-Sitecore</id><content type="html" xml:base="https://kezhan.info/2022/11/19/403-forbidden-errors-when-deploying-JSS-apps-to-Sitecore.html"><![CDATA[<div>Getting started with Headless development using Sitecore Headless SDKs and Docker are very easy. All you need to do is to</div>
<ol><li><div>Create a Headless app using command "npx create-sitecore-jss@ver20" following the instructions <a target="_blank" href="https://doc.sitecore.com/xp/en/developers/hd/201/sitecore-headless-development/create-a-jss-project-for-the-latest-versions-of-jss-and-sitecore.html">here</a></div></li><li><div>When you want to connect with a real Sitecore instance running on Docker, just use one of the <a target="_blank" href="https://github.com/Sitecore/docker-examples" rev="en_rl_none">docker-examples</a> then connect following the examples <a target="_blank" href="https://doc.sitecore.com/xp/en/developers/hd/201/sitecore-headless-development/walkthrough--connecting-a-jss-application-to-sitecore.html">here</a> (It is a remote Sitecore instance in this case)</div></li></ol>
<div><br /></div>
<h2>The Issue</h2>
<div><br /></div>
<div>When deploying the JSS app to Sitecore using the command</div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>jss deploy app -c -d</div></div>
<div>You may get the following errors:</div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>Wrote sitecore\package\sitecore-jss-app.xxxxxxxxx.manifest.zip </div><div>Sending package sitecore\package\sitecore-jss-app.xxxxxxxxx.manifest.zip to https://cm.dockerexamples.localhost/sitecore/api/jss/import... </div><div>Unexpected response from import service: </div><div>Status message: Forbidden </div><div>Status: 403</div></div>
<div><br /></div>
<h2>Troubleshooting</h2>
<div><br /></div>
<ol><li><div>Set "debugSecurity" to true in "\App_Config\Include\zzz\sitecore-jss-app.deploysecret.config" file that is deployed to your Sitecore instance.</div></li><li><div>Then run the jss deploy app command again with "--debugSecurity" like below</div></li></ol>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>jss deploy app -c -d --debugSecurity</div></div>
<div><br /></div>
<div>You can then check the logs in jss deploy app command comparing to the logs from the Sitecore instance. You will see the difference here is caused by "Deployment security factors" in jss and "Server-side security factors" in Sitecore CM have the URLs in https vs http.</div>
<div><br /></div>
<div>This is a security feature and the reason to our issue being that instances in docker are exposed via traefik on https when accessed from host machine, but within docker the request is received on http.</div>
<div><br /></div>
<div>The fix is to make the "security factors" match from the both sides.</div>
<div><br /></div>
<h2>Solutions</h2>
<div><br /></div>
<div>There are 2 solutions to this. One is more hacky but quicker than the other one.</div>
<div><br /></div>
<h3>Solution 1: The hacky and quick one. Change the npm package so it is requesting on https but generates the security headers in http.</h3>
<div><br /></div>
<ol><li><div>In your JSS app</div></li><li><div>Find file "\node_modules\@sitecore-jss\sitecore-jss-dev-tools\dist\cjs\package-deploy.js",</div></li><li><div>Find the 2 lines that start with "const factors ="</div></li><li><div>Append ".replace("https","http")" after "options.importServiceUrl"</div></li></ol>
<div><br /></div>
<h3>Solution 2: The more proper one. Enable http on traefik for CM in docker setup.</h3>
<div><br /></div>
<ol><li><div>In your Sitecore "docker-compose.override.yml" file</div></li><ol><li><div>Add below to cm, then restart docker containers</div></li></ol></ol>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>&nbsp; &nbsp; ports: </div><div>&nbsp; &nbsp; &nbsp; - 80:80</div></div>
<ol start="3"><li><div>In the JSS app, scjssconfig.json file</div></li><ol><li><div>Change the "deployUrl" to <a target="_blank" href="http://localhost/sitecore/api/jss/import">http://localhost/sitecore/api/jss/import</a></div></li></ol></ol>
<div><br /></div>
<div>The solution requires port 80 not being used in your local machine. Which should be the case if you have worked with Sitecore in docker for a while locally or planning to do so. If not, using a different port number is actually very tricky and can easily go down a rabbit hole. Some readings here:</div>
<ul><li><div><a target="_blank" href="https://github.com/Sitecore/jss/issues/369">https://github.com/Sitecore/jss/issues/369</a></div></li><li><div><a target="_blank" href="https://www.maartenwillebrands.nl/2022/04/13/sitecore-running-sitecore-in-docker-on-a-non-default-port/">https://www.maartenwillebrands.nl/2022/04/13/sitecore-running-sitecore-in-docker-on-a-non-default-port/</a></div></li></ul>
<div>Which I'd rather not to because the point of using Sitecore with docker is to allow quick and efficient ways to set up local environments and to start development right away.</div>
<div><br /></div>
<div><b>Notes: </b>All above are based on Sitecore 10.2 XP0 and Sitecore JSS NextJS 20.1.3</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Getting started with Headless development using Sitecore Headless SDKs and Docker are very easy. All you need to do is to Create a Headless app using command "npx create-sitecore-jss@ver20" following the instructions hereWhen you want to connect with a real Sitecore instance running on Docker, just use one of the docker-examples then connect following the examples here (It is a remote Sitecore instance in this case) The Issue When deploying the JSS app to Sitecore using the command jss deploy app -c -d You may get the following errors: Wrote sitecore\package\sitecore-jss-app.xxxxxxxxx.manifest.zip Sending package sitecore\package\sitecore-jss-app.xxxxxxxxx.manifest.zip to https://cm.dockerexamples.localhost/sitecore/api/jss/import... Unexpected response from import service: Status message: Forbidden Status: 403 Troubleshooting Set "debugSecurity" to true in "\App_Config\Include\zzz\sitecore-jss-app.deploysecret.config" file that is deployed to your Sitecore instance.Then run the jss deploy app command again with "--debugSecurity" like below jss deploy app -c -d --debugSecurity You can then check the logs in jss deploy app command comparing to the logs from the Sitecore instance. You will see the difference here is caused by "Deployment security factors" in jss and "Server-side security factors" in Sitecore CM have the URLs in https vs http. This is a security feature and the reason to our issue being that instances in docker are exposed via traefik on https when accessed from host machine, but within docker the request is received on http. The fix is to make the "security factors" match from the both sides. Solutions There are 2 solutions to this. One is more hacky but quicker than the other one. Solution 1: The hacky and quick one. Change the npm package so it is requesting on https but generates the security headers in http. In your JSS appFind file "\node_modules\@sitecore-jss\sitecore-jss-dev-tools\dist\cjs\package-deploy.js",Find the 2 lines that start with "const factors ="Append ".replace("https","http")" after "options.importServiceUrl" Solution 2: The more proper one. Enable http on traefik for CM in docker setup. In your Sitecore "docker-compose.override.yml" fileAdd below to cm, then restart docker containers &nbsp; &nbsp; ports: &nbsp; &nbsp; &nbsp; - 80:80 In the JSS app, scjssconfig.json fileChange the "deployUrl" to http://localhost/sitecore/api/jss/import The solution requires port 80 not being used in your local machine. Which should be the case if you have worked with Sitecore in docker for a while locally or planning to do so. If not, using a different port number is actually very tricky and can easily go down a rabbit hole. Some readings here: https://github.com/Sitecore/jss/issues/369https://www.maartenwillebrands.nl/2022/04/13/sitecore-running-sitecore-in-docker-on-a-non-default-port/ Which I'd rather not to because the point of using Sitecore with docker is to allow quick and efficient ways to set up local environments and to start development right away. Notes: All above are based on Sitecore 10.2 XP0 and Sitecore JSS NextJS 20.1.3]]></summary></entry><entry><title type="html">Sitecore CDP Batch API Import with PowerShell scripts</title><link href="https://kezhan.info/2022/05/10/Sitecore-CDP-Batch-API-Import-with-PowerShell-scripts.html" rel="alternate" type="text/html" title="Sitecore CDP Batch API Import with PowerShell scripts" /><published>2022-05-10T00:00:00+00:00</published><updated>2022-05-10T00:00:00+00:00</updated><id>https://kezhan.info/2022/05/10/Sitecore-CDP-Batch-API-Import-with-PowerShell-scripts</id><content type="html" xml:base="https://kezhan.info/2022/05/10/Sitecore-CDP-Batch-API-Import-with-PowerShell-scripts.html"><![CDATA[<div>To import a batch file into Sitecore CDP using the Batch API, these are the steps</div>
<ol><li><div>Create a batch file following the correct format</div></li><ol><li><div>Format: <a target="_blank" href="https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/sitecore-cdp-batch-file-format-requirements.html" rev="en_rl_none">https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/sitecore-cdp-batch-file-format-requirements.html</a></div></li><li><div>Example: Guest data model format: <a target="_blank" href="https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/sitecore-cdp-guest-data-model-for-batch-api.html" rev="en_rl_none">https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/sitecore-cdp-guest-data-model-for-batch-api.html</a></div></li><ol><li><div>No "Insert" is support, only "Upsert"!</div></li></ol><li><div>The file itself is not a valid JSON! It's multiple JSON in a single file, each JSON content are on a single line!</div></li></ol><li><div>"gzip" the batch file</div></li><ol><li><div>On Windows, can just use 7zip</div></li><li><div>Or the PowerShell scripts below</div></li></ol><li><div>Get MD5 checksum and the file size in byte</div></li><ol><li><div>Use the PowerShell scripts below</div></li><li><div>Or use this online tool: <a target="_blank" href="https://emn178.github.io/online-tools/md5_checksum.html" rev="en_rl_none">https://emn178.github.io/online-tools/md5_checksum.html</a></div></li></ol><li><div>Batch API call: Create the batch and get the AWS upload URL using the Batch API</div></li><ol><li><div>PUT method</div></li><li><div>Generate a new UUID for identifying your import and use this UUID on your API call: <a target="_blank" href="https://api.boxever.com/v2/batches/[[Your" rev="en_rl_none">https://api.boxever.com/v2/batches/[[Your</a> UUID]]</div></li><li><div>Use your api key and secret in basic authentication as username and password </div></li><li><div>MD5 checksum value should be lower case!</div></li><li><div>Request details can be found here: <a target="_blank" href="https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/importing-a-batch-file-into-sitecore-cdp.html" rev="en_rl_none">https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/importing-a-batch-file-into-sitecore-cdp.html</a></div></li></ol><li><div>Get base64 string of the MD5 checksum from HEX value</div></li><ol><li><div>Use the PowerShell scripts below</div></li><li><div>Or use this online tool: <a target="_blank" href="https://base64.guru/converter/encode/hex" rev="en_rl_none">https://base64.guru/converter/encode/hex</a></div></li></ol><li><div>AWS API call: Upload the gzip file to AWS upload URL</div></li><ol><li><div>Headers:</div></li><ol><li><div><span style="color:rgb(85, 85, 85);">Content-Md5: use the value from the previous step</span></div></li><li><div>x-amz-server-side-encryption: <span style="color:rgb(85, 85, 85);">AES256</span></div></li></ol></ol><li><div>Batch API call: Check the status of the batch import process</div></li><li><div>View error logs if the import failed</div></li><ol><li><div>Open the link in browser, it downloads as "gz" file. Unzip and can check individual errors</div></li><li><div>Common Errors</div></li><ol><li><div>{"ref":"1e20d1e1-07f0-4863-92f1-4d2e15c86a59","code":"400","message":"Not enough identifying information"}</div></li><ol><li><div>Enough fields must be provided so the customer can be identified</div></li></ol><li><div>{"ref":"null","code":"400","message":"Failed to parse import line"}</div></li><ol><li><div>Ensure that one JSON is flatted out in a single line, do not beautify the JSON!</div></li></ol><li><div>If the batch status says corrupted, ensure that MD5 checksum is correct and lower case!</div></li></ol></ol></ol>
<div><br /></div>
<div><b>Below is a PowerShell Scripts that</b></div>
<ol><li><div>Generates a new UUID</div></li><li><div>"gzip" the batch file</div></li><li><div>Outputs the file size of the gzip file in bytes</div></li><li><div>Outputs the lower case version of the MD5 checksum of the gzip file</div></li><li><div>Outputs the base64 value from the HEX value of the MD5 checksum</div></li></ol>
<div><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>[CmdletBinding()] </div><div>param ( </div><div>&nbsp; &nbsp; [string] </div><div>&nbsp; &nbsp; $filePath </div><div>) </div><div>Function Gzip-File([ValidateScript({ Test-Path $_ })][string]$File) { </div><div>&nbsp; &nbsp; $srcFile = Get-Item -Path $File </div><div>&nbsp; &nbsp; $newFileName = "$($srcFile.FullName).gz" </div><div>&nbsp; &nbsp; try { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; $srcFileStream = New-Object System.IO.FileStream($srcFile.FullName, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read)) </div><div>&nbsp; &nbsp; &nbsp; &nbsp; $dstFileStream = New-Object System.IO.FileStream($newFileName, ([IO.FileMode]::Create), ([IO.FileAccess]::Write), ([IO.FileShare]::None)) </div><div>&nbsp; &nbsp; &nbsp; &nbsp; $gzip = New-Object System.IO.Compression.GZipStream($dstFileStream, [System.IO.Compression.CompressionMode]::Compress) </div><div>&nbsp; &nbsp; &nbsp; &nbsp; $srcFileStream.CopyTo($gzip) </div><div>&nbsp; &nbsp; }&nbsp; </div><div>&nbsp; &nbsp; catch { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; Write-Host "$_.Exception.Message" -ForegroundColor Red </div><div>&nbsp; &nbsp; } </div><div>&nbsp; &nbsp; finally { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; $gzip.Dispose() </div><div>&nbsp; &nbsp; &nbsp; &nbsp; $srcFileStream.Dispose() </div><div>&nbsp; &nbsp; &nbsp; &nbsp; $dstFileStream.Dispose() </div><div>&nbsp; &nbsp; } </div><div>} </div><div>Write-Host "UUID: " -ForegroundColor Green </div><div>(New-Guid).Guid </div><div>Gzip-File $filePath </div><div>$gfilePath = $filePath + ".gz" </div><div>Write-Host "File Size: " -ForegroundColor Green </div><div>(Get-Item $gfilePath ).length </div><div>$md5 = Get-FileHash -Path $gfilePath -Algorithm MD5 </div><div>$hash = $md5.Hash.ToLower() </div><div>Write-Host "MD5 Hash: " -ForegroundColor Green </div><div>$hash </div><div>$bytes = [byte[]] -split ($hash -replace '..', '0x$&amp; ') </div><div>Write-Host "Content-Md5: " -ForegroundColor Green </div><div>[System.Convert]::ToBase64String($bytes)</div></div>
<div><br /></div>
<div>Save above as PrepBatch.ps1 and invoke like: </div>
<div>.\PrepBatch.ps1 .\input.batch</div>
<div><br /></div>
<div>With the values generated, you can use them with tools like Postman to send all requests.</div>]]></content><author><name></name></author><summary type="html"><![CDATA[To import a batch file into Sitecore CDP using the Batch API, these are the steps Create a batch file following the correct formatFormat: https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/sitecore-cdp-batch-file-format-requirements.htmlExample: Guest data model format: https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/sitecore-cdp-guest-data-model-for-batch-api.htmlNo "Insert" is support, only "Upsert"!The file itself is not a valid JSON! It's multiple JSON in a single file, each JSON content are on a single line!"gzip" the batch fileOn Windows, can just use 7zipOr the PowerShell scripts belowGet MD5 checksum and the file size in byteUse the PowerShell scripts belowOr use this online tool: https://emn178.github.io/online-tools/md5_checksum.htmlBatch API call: Create the batch and get the AWS upload URL using the Batch APIPUT methodGenerate a new UUID for identifying your import and use this UUID on your API call: https://api.boxever.com/v2/batches/[[Your UUID]]Use your api key and secret in basic authentication as username and password MD5 checksum value should be lower case!Request details can be found here: https://doc.sitecore.com/cdp/en/developers/sitecore-customer-data-platform--data-model-2-1/importing-a-batch-file-into-sitecore-cdp.htmlGet base64 string of the MD5 checksum from HEX valueUse the PowerShell scripts belowOr use this online tool: https://base64.guru/converter/encode/hexAWS API call: Upload the gzip file to AWS upload URLHeaders:Content-Md5: use the value from the previous stepx-amz-server-side-encryption: AES256Batch API call: Check the status of the batch import processView error logs if the import failedOpen the link in browser, it downloads as "gz" file. Unzip and can check individual errorsCommon Errors{"ref":"1e20d1e1-07f0-4863-92f1-4d2e15c86a59","code":"400","message":"Not enough identifying information"}Enough fields must be provided so the customer can be identified{"ref":"null","code":"400","message":"Failed to parse import line"}Ensure that one JSON is flatted out in a single line, do not beautify the JSON!If the batch status says corrupted, ensure that MD5 checksum is correct and lower case! Below is a PowerShell Scripts that Generates a new UUID"gzip" the batch fileOutputs the file size of the gzip file in bytesOutputs the lower case version of the MD5 checksum of the gzip fileOutputs the base64 value from the HEX value of the MD5 checksum [CmdletBinding()] param ( &nbsp; &nbsp; [string] &nbsp; &nbsp; $filePath ) Function Gzip-File([ValidateScript({ Test-Path $_ })][string]$File) { &nbsp; &nbsp; $srcFile = Get-Item -Path $File &nbsp; &nbsp; $newFileName = "$($srcFile.FullName).gz" &nbsp; &nbsp; try { &nbsp; &nbsp; &nbsp; &nbsp; $srcFileStream = New-Object System.IO.FileStream($srcFile.FullName, ([IO.FileMode]::Open), ([IO.FileAccess]::Read), ([IO.FileShare]::Read)) &nbsp; &nbsp; &nbsp; &nbsp; $dstFileStream = New-Object System.IO.FileStream($newFileName, ([IO.FileMode]::Create), ([IO.FileAccess]::Write), ([IO.FileShare]::None)) &nbsp; &nbsp; &nbsp; &nbsp; $gzip = New-Object System.IO.Compression.GZipStream($dstFileStream, [System.IO.Compression.CompressionMode]::Compress) &nbsp; &nbsp; &nbsp; &nbsp; $srcFileStream.CopyTo($gzip) &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; catch { &nbsp; &nbsp; &nbsp; &nbsp; Write-Host "$_.Exception.Message" -ForegroundColor Red &nbsp; &nbsp; } &nbsp; &nbsp; finally { &nbsp; &nbsp; &nbsp; &nbsp; $gzip.Dispose() &nbsp; &nbsp; &nbsp; &nbsp; $srcFileStream.Dispose() &nbsp; &nbsp; &nbsp; &nbsp; $dstFileStream.Dispose() &nbsp; &nbsp; } } Write-Host "UUID: " -ForegroundColor Green (New-Guid).Guid Gzip-File $filePath $gfilePath = $filePath + ".gz" Write-Host "File Size: " -ForegroundColor Green (Get-Item $gfilePath ).length $md5 = Get-FileHash -Path $gfilePath -Algorithm MD5 $hash = $md5.Hash.ToLower() Write-Host "MD5 Hash: " -ForegroundColor Green $hash $bytes = [byte[]] -split ($hash -replace '..', '0x$&amp; ') Write-Host "Content-Md5: " -ForegroundColor Green [System.Convert]::ToBase64String($bytes) Save above as PrepBatch.ps1 and invoke like: .\PrepBatch.ps1 .\input.batch With the values generated, you can use them with tools like Postman to send all requests.]]></summary></entry><entry><title type="html">Internal authority URL with OpenID Connect in docker for ASP.NET Core</title><link href="https://kezhan.info/2021/08/04/Internal-authority-URL-with-OpenID-Connect-in-docker-for-ASPNET-Core.html" rel="alternate" type="text/html" title="Internal authority URL with OpenID Connect in docker for ASP.NET Core" /><published>2021-08-04T00:00:00+00:00</published><updated>2021-09-01T00:00:00+00:00</updated><id>https://kezhan.info/2021/08/04/Internal-authority-URL-with-OpenID-Connect-in-docker-for-ASPNET-Core</id><content type="html" xml:base="https://kezhan.info/2021/08/04/Internal-authority-URL-with-OpenID-Connect-in-docker-for-ASPNET-Core.html"><![CDATA[<div>When setting up OpenID Connect with ASP.NET Core in a docker container environment, you may encounter issues with the public authority URL being not accessible inside of the container, causing the setup to fail.</div>
<div><br /></div>
<div>In my test case, it's a Sitecore 10.1 XP environment with Sitecore Identity. Internal Identity URL is http://id but the external Identity URL is https://identity.clientname.com.</div>
<div><br /></div>
<div>This is only one of the possible solutions.</div>
<div><br /></div>
<div>In summary, this solution is to hijack when requests are made in the application related to OpenID connect, and replace external authority URLs with internal ones.</div>
<div><br /></div>
<div>I am not covering the overall setup of OpenID connect in ASP.NET Core, there are enough articles about it on the internet already.</div>
<div><br /></div>
<div>Create a custom HttpClientHandler like below:</div>
<div><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>using System; </div><div>using System.Net.Http; </div><div>using System.Threading; </div><div>using System.Threading.Tasks;</div><div><br /></div><div> </div><div> </div><div>namespace Custom </div><div>{ </div><div>&nbsp; &nbsp; /// &lt;summary&gt; </div><div>&nbsp; &nbsp; /// Handles all the backend channel communications to Identity and replace any public identity url with internal url </div><div>&nbsp; &nbsp; /// &lt;/summary&gt; </div><div>&nbsp; &nbsp; public class CustomHttpMessageHandler : HttpClientHandler </div><div>&nbsp; &nbsp; { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; private string _authority { get; set; } </div><div>&nbsp; &nbsp; &nbsp; &nbsp; private string _internalAuthority { get; set; }</div><div><br /></div><div> </div><div> </div><div>&nbsp; &nbsp; &nbsp; &nbsp; public CustomHttpMessageHandler(string authority, string internalAuthority) </div><div>&nbsp; &nbsp; &nbsp; &nbsp; { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _authority = authority; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _internalAuthority = internalAuthority; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; }</div><div><br /></div><div> </div><div>&nbsp; &nbsp; &nbsp; &nbsp; protected override Task&lt;HttpResponseMessage&gt; SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) </div><div>&nbsp; &nbsp; &nbsp; &nbsp; { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; request.RequestUri = new Uri(request.RequestUri.OriginalString.Replace(_authority, _internalAuthority)); </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return base.SendAsync(request, cancellationToken); </div><div>&nbsp; &nbsp; &nbsp; &nbsp; } </div><div>&nbsp; &nbsp; } </div><div> </div><div>}</div></div>
<div><br /></div>
<div>In the startup.cs, replace the default BackchannelHttpHandler with the custom one:</div>
<div><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>...</div><div>public void ConfigureServices(IServiceCollection services) </div><div>&nbsp; &nbsp; &nbsp; &nbsp; { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var internalAuthority = "http://id"; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var authority = "https://identity.clientname.com"; </div><div><br /></div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...</div><div> </div><div> </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; services </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ... </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .AddOpenIdConnect(cfg =&gt; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cfg.Authority = authority; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ... </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cfg.RequireHttpsMetadata = false; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cfg.BackchannelHttpHandler = new CustomHttpMessageHandler(authority, internalAuthority);</div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...</div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cfg.Events.OnRedirectToIdentityProvider = async context =&gt; </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (context.ProtocolMessage.RedirectUri.StartsWith("http://")) </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace("http://", "https://"); </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; await Task.FromResult(0); </div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };</div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...</div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }</div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...</div><div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }</div><div>...</div></div>
<div><br /></div>
<div>Replace the URLs, ideally, get them from configurations. Note that the handling of http vs https since internal is normally http and external is https.</div>
<div><br /></div>
<div>After the setup, all the requests inside of docker related to OpenID connect will use the internal URL and browser URLs will be proper public https versions.</div>
<div><br /></div>
<div><span style="color:rgb(36, 41, 46);"><span style="--en-markholder:true;"><br /></span></span></div>
<div><br /></div>]]></content><author><name></name></author><summary type="html"><![CDATA[When setting up OpenID Connect with ASP.NET Core in a docker container environment, you may encounter issues with the public authority URL being not accessible inside of the container, causing the setup to fail. In my test case, it's a Sitecore 10.1 XP environment with Sitecore Identity. Internal Identity URL is http://id but the external Identity URL is https://identity.clientname.com. This is only one of the possible solutions. In summary, this solution is to hijack when requests are made in the application related to OpenID connect, and replace external authority URLs with internal ones. I am not covering the overall setup of OpenID connect in ASP.NET Core, there are enough articles about it on the internet already. Create a custom HttpClientHandler like below: using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace Custom { &nbsp; &nbsp; /// &lt;summary&gt; &nbsp; &nbsp; /// Handles all the backend channel communications to Identity and replace any public identity url with internal url &nbsp; &nbsp; /// &lt;/summary&gt; &nbsp; &nbsp; public class CustomHttpMessageHandler : HttpClientHandler &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; private string _authority { get; set; } &nbsp; &nbsp; &nbsp; &nbsp; private string _internalAuthority { get; set; } &nbsp; &nbsp; &nbsp; &nbsp; public CustomHttpMessageHandler(string authority, string internalAuthority) &nbsp; &nbsp; &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _authority = authority; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _internalAuthority = internalAuthority; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; protected override Task&lt;HttpResponseMessage&gt; SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) &nbsp; &nbsp; &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; request.RequestUri = new Uri(request.RequestUri.OriginalString.Replace(_authority, _internalAuthority)); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return base.SendAsync(request, cancellationToken); &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; } } In the startup.cs, replace the default BackchannelHttpHandler with the custom one: ...public void ConfigureServices(IServiceCollection services) &nbsp; &nbsp; &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var internalAuthority = "http://id"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var authority = "https://identity.clientname.com"; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ... &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; services &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ... &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .AddOpenIdConnect(cfg =&gt; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cfg.Authority = authority; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ... &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cfg.RequireHttpsMetadata = false; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cfg.BackchannelHttpHandler = new CustomHttpMessageHandler(authority, internalAuthority);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cfg.Events.OnRedirectToIdentityProvider = async context =&gt; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (context.ProtocolMessage.RedirectUri.StartsWith("http://")) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; context.ProtocolMessage.RedirectUri = context.ProtocolMessage.RedirectUri.Replace("http://", "https://"); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; await Task.FromResult(0); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }... Replace the URLs, ideally, get them from configurations. Note that the handling of http vs https since internal is normally http and external is https. After the setup, all the requests inside of docker related to OpenID connect will use the internal URL and browser URLs will be proper public https versions.]]></summary></entry><entry><title type="html">Switching to FastMail after 15 years of Gmail</title><link href="https://kezhan.info/2021/07/21/Switching-to-FastMail-after-15-years-of-Gmail.html" rel="alternate" type="text/html" title="Switching to FastMail after 15 years of Gmail" /><published>2021-07-21T00:00:00+00:00</published><updated>2021-09-03T00:00:00+00:00</updated><id>https://kezhan.info/2021/07/21/Switching-to-FastMail-after-15-years-of-Gmail</id><content type="html" xml:base="https://kezhan.info/2021/07/21/Switching-to-FastMail-after-15-years-of-Gmail.html"><![CDATA[<div>If you are not in the mood for a story, feel free to jump to the FastMail review section down below.</div>
<div><br /></div>
<div>Since I had my first Gmail account in 2006, over the years I have registered many more. Plus other Outlook and Office365 emails. I always wanted a centralised place online to access all the accounts but I never found a solution that I liked.</div>
<div><br /></div>
<div>Recently a Telegram bot became popular among online Chinese communities, it can query all the leaked personal data based on emails, website usernames, and mobile numbers etc. I have always known that things are being leaked online via various hacks but I never thought it is this serious. </div>
<div><br /></div>
<div>It had everything! My passwords, security questions, emails, mobile numbers, national identity number (of China) and rough locations I have surfed the internet over the years when I was in China.</div>
<div><br /></div>
<div>The conclusion I came to after that: to guarantee personal data security in this era</div>
<ul><li><div>For every service and website, different emails and passwords shall be used.</div></li><li><div>Security questions should also be dynamically generated passwords that are stored in a password manager.</div></li></ul>
<div><br /></div>
<div>I have been using LastPass for a long time so the password part is solved already. When it comes to email, not so much.</div>
<div><br /></div>
<div>I have about 15 emails addresses currently with Gmail and Outlook/Office365. One of the Gmail accounts is still the free legacy GSuite plan and is bind to a custom domain of mine. The problem with that domain is that it is a .US domain that doesn't support privacy protection and has to expose my personal contact information in WHOIS databases.</div>
<div><br /></div>
<div>Therefore the plan was to</div>
<ul><li><div>Get a new, not-too-long, cheap domain that supports WHOIS guard</div></li><li><div>Switch to a PAID email service for the custom domains and manage all email accounts in one single place. Then gradually change all emails to the custom ones.</div></li></ul>
<div><br /></div>
<div>FastMail seems to be a good choice. After trialling it for less than a day, I select the Standard plan and started migrating all my email accounts and setup. </div>
<div><br /></div>
<div>Here are my takes:</div>
<div><br /></div>
<h3>Migration</h3>
<div><b><span style="--en-markholder:true;"><br /></span></b></div>
<ul><li><div>Emails from 15 Gmail and Outlook/Office365 accounts were imported. Can not import iCloud emails, only contacts and calendars. The migration processes were smooth.</div></li></ul>
<div><br /></div>
<div><br /></div>
<h3>Using Other Emails in One Place</h3>
<div><br /></div>
<ul><li><div>IMAP and SMTP can be set up automatically when importing email accounts.</div></li><li><div>IMAP default poll interval is 1 hour when the web interface is not open. It's a bit long. So I changed all email accounts to forwarding, this allows me to set up rules to mark them as read as well. </div></li><ul><li><div>IMAP settings can be disabled without deleting them.</div></li><li><div>Note that if an email is marked as spam in the original email account, it won't be forwarded. Since normally spam are deleted in 30 days, there is a slight chance of losing important emails. Very slight...</div></li><li><div>Since SMTP was set up during import, you still can send emails with those accounts.</div></li></ul></ul>
<div><br /></div>
<div><br /></div>
<h3>FastMail Features</h3>
<div><br /></div>
<div>Speed</div>
<ul><li><div>FastMail is not fast... It is not slow, but the web interface is sometimes stuck. iOS app seems to be an advanced web app so sometimes it loads for a while. That being said, it's not an issue. It's fast enough, just not lightning fast.</div></li></ul>
<div><br /></div>
<div>Custom Domains</div>
<ul><li><div>I set up 2 custom domains. One of them has DNS servers set to FastMail's so the setup was really easy. And since they manage the DNS, it makes setup easier for other features too like sub-domains and websites etc...</div></li></ul>
<div><br /></div>
<div>Websites</div>
<ul><li><div>This was a surprise to me since I didn't know they had this feature. Basically allows you to host static websites under your custom domains or your FastMail accounts. When DNS is managed by them as mentioned above, no CNAME setup is required at all. With just a few clicks, the website is up.</div></li><li><div>Not much use for me at this point, but I am sure this would come in handy one day.</div></li></ul>
<div><br /></div>
<div>Labels v.s. Folders</div>
<ul><li><div>You can choose to use Labels or Folders. I chose Labels since I like to mark emails with multiple labels and labels can be nested too so still support a tree structure.</div></li></ul>
<div><br /></div>
<div>Storage</div>
<ul><li><div>I used about 6GB for all emails. The Standard plan comes with 30GB, which should be enough for me for the next few decades...</div></li></ul>
<div><br /></div>
<div>Email Clients</div>
<ul><li><div>The setup on iPhone was just a scan of a QR code! It sets up email, calendar, contacts, notes and reminders altogether. So I can replace all the Google services on the phone which is great. However I couldn't find reminders from iOS on the web interface, so I disabled the feature on iPhone.</div></li><li><div>Haven't tested other email clients yet but I guess they should all work fine?</div></li></ul>
<div><br /></div>
<div>Labelling wildcard emails use To address</div>
<ul><li><div>One of the main benefits of having a custom domain email for me is to be able to use any email address with that domain and all the emails go into one single mailbox. It is very handy so I can use different email addresses for different services/websites without any pre-setup required.</div><div><br /></div><div>It does create a challenge to identify which email is for which address when looking at the mailbox. So in Google, I built App Scripts to automatically label incoming emails with the email address from To address.</div><div><br /></div><div>FastMail doesn't support this out of the box. However, they do support the Sieve code and it seems to be powerful enough to do the things I wanted. The problem is that I have never heard of it before and am still figuring out how to write the script. I am also trying to get help from their Support for this. Anyway, this wouldn't be a big issue if it can't be done automatically. I can always manually set up the rules afterwards.</div></li></ul>
<div style="padding-left:40px;"><br /></div>
<div style="padding-left:40px;"><b>Updates</b>: Fastmail support actually helped me with the Sieve code that's required and it does exactly what I wanted!</div>
<div style="padding-left:40px;"><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>if header :matches "To" "*@domain.tld"</div><div>{</div><div>set "foldername" "${1}";</div><div>fileinto :copy :create "INBOX.${foldername}";</div><div>stop;</div><div>}​</div></div>
<div><br /></div>
<div>Search</div>
<ul><li><div><s>Couldn't figure out a way to search for emails without any labels assigned yet...&nbsp; </s> Use "in:Archive" in search would give all emails with any labels!</div></li><li><div>There was one time in iOS, old emails that are irrelevant show up in front in search results. Couldn't replicate on the web or the 2nd day in iOS. Could be a bug...</div></li></ul>
<div><br /></div>
<div>Support</div>
<ul><li><div>Got a reply overnight, although my question was misunderstood, which could be my bad... Anyway, the response time is good.</div></li><ul><li><div><b>Updates</b>: they actually helped me with the Sieve code that implements the custom feature I wanted, very nice and helpful!</div></li></ul></ul>
<div><br /></div>
<div><br /></div>
<div>Overall, I am happy with what I have so far. The Standard plan costs about 76 AUD a year which is very inexpensive. I didn't mention anything about security and privacy because, to be honest, there is no way for me to verify,&nbsp; just have to take their words for it.</div>
<div><br /></div>
<div>The only thing left for me to do now is to change all emails in different services and websites. I think this is going to take a while, 10 years maybe...</div>
<div><br /></div>
<div><b>Updates</b>: been using it for almost two months now, still happy.</div>
<div><br /></div>]]></content><author><name></name></author><summary type="html"><![CDATA[If you are not in the mood for a story, feel free to jump to the FastMail review section down below. Since I had my first Gmail account in 2006, over the years I have registered many more. Plus other Outlook and Office365 emails. I always wanted a centralised place online to access all the accounts but I never found a solution that I liked. Recently a Telegram bot became popular among online Chinese communities, it can query all the leaked personal data based on emails, website usernames, and mobile numbers etc. I have always known that things are being leaked online via various hacks but I never thought it is this serious. It had everything! My passwords, security questions, emails, mobile numbers, national identity number (of China) and rough locations I have surfed the internet over the years when I was in China. The conclusion I came to after that: to guarantee personal data security in this era For every service and website, different emails and passwords shall be used.Security questions should also be dynamically generated passwords that are stored in a password manager. I have been using LastPass for a long time so the password part is solved already. When it comes to email, not so much. I have about 15 emails addresses currently with Gmail and Outlook/Office365. One of the Gmail accounts is still the free legacy GSuite plan and is bind to a custom domain of mine. The problem with that domain is that it is a .US domain that doesn't support privacy protection and has to expose my personal contact information in WHOIS databases. Therefore the plan was to Get a new, not-too-long, cheap domain that supports WHOIS guardSwitch to a PAID email service for the custom domains and manage all email accounts in one single place. Then gradually change all emails to the custom ones. FastMail seems to be a good choice. After trialling it for less than a day, I select the Standard plan and started migrating all my email accounts and setup. Here are my takes: Migration Emails from 15 Gmail and Outlook/Office365 accounts were imported. Can not import iCloud emails, only contacts and calendars. The migration processes were smooth. Using Other Emails in One Place IMAP and SMTP can be set up automatically when importing email accounts.IMAP default poll interval is 1 hour when the web interface is not open. It's a bit long. So I changed all email accounts to forwarding, this allows me to set up rules to mark them as read as well. IMAP settings can be disabled without deleting them.Note that if an email is marked as spam in the original email account, it won't be forwarded. Since normally spam are deleted in 30 days, there is a slight chance of losing important emails. Very slight...Since SMTP was set up during import, you still can send emails with those accounts. FastMail Features Speed FastMail is not fast... It is not slow, but the web interface is sometimes stuck. iOS app seems to be an advanced web app so sometimes it loads for a while. That being said, it's not an issue. It's fast enough, just not lightning fast. Custom Domains I set up 2 custom domains. One of them has DNS servers set to FastMail's so the setup was really easy. And since they manage the DNS, it makes setup easier for other features too like sub-domains and websites etc... Websites This was a surprise to me since I didn't know they had this feature. Basically allows you to host static websites under your custom domains or your FastMail accounts. When DNS is managed by them as mentioned above, no CNAME setup is required at all. With just a few clicks, the website is up.Not much use for me at this point, but I am sure this would come in handy one day. Labels v.s. Folders You can choose to use Labels or Folders. I chose Labels since I like to mark emails with multiple labels and labels can be nested too so still support a tree structure. Storage I used about 6GB for all emails. The Standard plan comes with 30GB, which should be enough for me for the next few decades... Email Clients The setup on iPhone was just a scan of a QR code! It sets up email, calendar, contacts, notes and reminders altogether. So I can replace all the Google services on the phone which is great. However I couldn't find reminders from iOS on the web interface, so I disabled the feature on iPhone.Haven't tested other email clients yet but I guess they should all work fine? Labelling wildcard emails use To address One of the main benefits of having a custom domain email for me is to be able to use any email address with that domain and all the emails go into one single mailbox. It is very handy so I can use different email addresses for different services/websites without any pre-setup required.It does create a challenge to identify which email is for which address when looking at the mailbox. So in Google, I built App Scripts to automatically label incoming emails with the email address from To address.FastMail doesn't support this out of the box. However, they do support the Sieve code and it seems to be powerful enough to do the things I wanted. The problem is that I have never heard of it before and am still figuring out how to write the script. I am also trying to get help from their Support for this. Anyway, this wouldn't be a big issue if it can't be done automatically. I can always manually set up the rules afterwards. Updates: Fastmail support actually helped me with the Sieve code that's required and it does exactly what I wanted! if header :matches "To" "*@domain.tld"{set "foldername" "${1}";fileinto :copy :create "INBOX.${foldername}";stop;}​ Search Couldn't figure out a way to search for emails without any labels assigned yet...&nbsp; Use "in:Archive" in search would give all emails with any labels!There was one time in iOS, old emails that are irrelevant show up in front in search results. Couldn't replicate on the web or the 2nd day in iOS. Could be a bug... Support Got a reply overnight, although my question was misunderstood, which could be my bad... Anyway, the response time is good.Updates: they actually helped me with the Sieve code that implements the custom feature I wanted, very nice and helpful! Overall, I am happy with what I have so far. The Standard plan costs about 76 AUD a year which is very inexpensive. I didn't mention anything about security and privacy because, to be honest, there is no way for me to verify,&nbsp; just have to take their words for it. The only thing left for me to do now is to change all emails in different services and websites. I think this is going to take a while, 10 years maybe... Updates: been using it for almost two months now, still happy.]]></summary></entry><entry><title type="html">Sitecore SXA Sites Generate URLs with Incorrect Hostnames</title><link href="https://kezhan.info/2021/05/26/Sitecore-SXA-Sites-Generate-URLs-with-Incorrect-Hostnames.html" rel="alternate" type="text/html" title="Sitecore SXA Sites Generate URLs with Incorrect Hostnames" /><published>2021-05-26T00:00:00+00:00</published><updated>2021-07-21T00:00:00+00:00</updated><id>https://kezhan.info/2021/05/26/Sitecore-SXA-Sites-Generate-URLs-with-Incorrect-Hostnames</id><content type="html" xml:base="https://kezhan.info/2021/05/26/Sitecore-SXA-Sites-Generate-URLs-with-Incorrect-Hostnames.html"><![CDATA[<div>Came across a weird issue with a client's Sitecore instance recently where on CD instances, internal link URLs generated in Navigation or redirects are absolute URLs including hostnames but they are CM hostname instead of CDs'.</div>
<div><br /></div>
<div>The Sitecore version is 10.1. SXA and Commerce Storefront. On Docker and AKS.</div>
<div><br /></div>
<div>After lots of digging and debugging Sitecore DLLs within docker containers, found the issue and another finding.</div>
<div><br /></div>
<ol><li><h3>CD generates URLs with CM hostname is because SXA site settings don't have a language set</h3></li></ol>
<div><br /></div>
<div>Debugging through the code of <b>LinkManager.GetItemUrl</b>, it reaches <b>Sitecore.Links.UrlBuilders.Helpers.OptionsDecoratedSiteResolver.ResolveSite </b>at some point like below:</div>
<div><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>// Sitecore.Links.UrlBuilders.Helpers.OptionsDecoratedSiteResolver&nbsp; </div><div>using Sitecore.Data.Items;&nbsp; </div><div>using Sitecore.Diagnostics;&nbsp; </div><div>using Sitecore.Web;&nbsp; </div><div>/// &lt;inheritdoc /&gt;&nbsp; </div><div>/// &lt;summary&gt;&nbsp; </div><div>/// Resolves the site that best matches the given item.&nbsp; </div><div>/// &lt;/summary&gt;&nbsp; </div><div>/// &lt;param name="item"&gt;Requested item that we need to resolve matching site.&lt;/param&gt;&nbsp; </div><div>/// &lt;returns&gt;&lt;/returns&gt;&nbsp; </div><div>public SiteInfo ResolveSite(Item item)&nbsp; </div><div>{&nbsp; </div><div>	Assert.ArgumentNotNull(item, "item");&nbsp; </div><div>	SiteInfo defaultSiteInfo = GetDefaultSiteInfo();&nbsp; </div><div>	if (SkipResolving(item))&nbsp; </div><div>	{&nbsp; </div><div>		return defaultSiteInfo;&nbsp; </div><div>	}&nbsp; </div><div>	return _realSiteResolver.ResolveSite(item) ?? defaultSiteInfo;&nbsp; </div><div>}</div></div>
<div><br /></div>
<div>if <b>SkipResolving </b>is true, it takes site context from the default which is correct. </div>
<div><br /></div>
<div>However, it is false in this case and it goes to <b>SiteResolver </b>to resolve the site via item only. So there is a chance that the item is resolved to the wrong site since both CM and CD are pointing to the same Sitecore item path and who gets resolved is all dependent on the order of the sites.</div>
<div><br /></div>
<div>But why <b>SkipResolving </b>is false?</div>
<div><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>// Sitecore.Links.UrlBuilders.Helpers.OptionsDecoratedSiteResolver&nbsp; </div><div>using Sitecore.Data.Items;&nbsp; </div><div>private bool SkipResolving(Item item)&nbsp; </div><div>{&nbsp; </div><div>	if (!NoNeedToResolveSite(item) &amp;&amp; !OptionsSiteIsDifferentThanContextSite())&nbsp; </div><div>	{&nbsp; </div><div>		return ItemMatchesCurrentSite(item);&nbsp; </div><div>	}&nbsp; </div><div>	return true;&nbsp; </div><div>}</div><div>.....</div><div>protected virtual bool ItemMatchesCurrentSite(Item item) </div><div>{ </div><div>	if (!Settings.Rendering.SiteResolvingMatchCurrentSite) </div><div>	{ </div><div>		return false; </div><div>	} </div><div>	if (PathMatchesContextSite(item)) </div><div>	{ </div><div>		return LanguageMatchesContextSite(item); </div><div>	} </div><div>	return false; </div><div>}</div><div>.....</div><div>private bool LanguageMatchesContextSite(Item item) </div><div>{ </div><div>	string name = item.Language.Name; </div><div>	string language = ContextSite.Language; </div><div>	if (Settings.Rendering.SiteResolvingMatchCurrentLanguage) </div><div>	{ </div><div>		return name.Equals(language, StringComparison.InvariantCultureIgnoreCase); </div><div>	} </div><div>	return true; </div><div>}</div></div>
<div><br /></div>
<div>Out of all the code here, <b>LanguageMatchesContextSite </b>is false because of <b>ContextSite.Language</b> is empty but <b>item.Language.Name</b> is "en".</div>
<div><br /></div>
<div>It's all because "<b>Language</b>" field of site settings item(such as "/sitecore/content/tenant/Sites/siteA/Settings/Site Grouping/siteA-CM") is blank... After setting this field to "en", the issue is no longer presented. </div>
<div><br /></div>
<ol start="2"><li><h3>Must set "TargetHostname" if "Hostname" contains pipes or wildcards in SXA site settings</h3></li></ol>
<div><br /></div>
<div>This one was not causing a direct issue however could potentially be one. The <b>GetTargetHostName </b>method on <b>SiteInfo </b>is like below:</div>
<div><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>// Sitecore.Web.SiteInfo </div><div>/// &lt;summary&gt; </div><div>/// Gets the name of the target host. </div><div>/// &lt;/summary&gt; </div><div>internal virtual string GetTargetHostName() </div><div>{ </div><div>	if (!string.IsNullOrEmpty(TargetHostName)) </div><div>	{ </div><div>		return TargetHostName; </div><div>	} </div><div>	if (HostName.IndexOfAny(new char[2] </div><div>	{ </div><div>		'*', </div><div>		'|' </div><div>	}) &lt; 0) </div><div>	{ </div><div>		return HostName; </div><div>	} </div><div>	return string.Empty; </div><div>}</div></div>
<div><br /></div>
<div>It's easy to see that if <b>Hostname </b>contains any wildcards or pipes and <b>TargetHostName </b>is blank, it would just return an empty string. Which would definitely cause issues somewhere on the site.</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Came across a weird issue with a client's Sitecore instance recently where on CD instances, internal link URLs generated in Navigation or redirects are absolute URLs including hostnames but they are CM hostname instead of CDs'. The Sitecore version is 10.1. SXA and Commerce Storefront. On Docker and AKS. After lots of digging and debugging Sitecore DLLs within docker containers, found the issue and another finding. CD generates URLs with CM hostname is because SXA site settings don't have a language set Debugging through the code of LinkManager.GetItemUrl, it reaches Sitecore.Links.UrlBuilders.Helpers.OptionsDecoratedSiteResolver.ResolveSite at some point like below: // Sitecore.Links.UrlBuilders.Helpers.OptionsDecoratedSiteResolver&nbsp; using Sitecore.Data.Items;&nbsp; using Sitecore.Diagnostics;&nbsp; using Sitecore.Web;&nbsp; /// &lt;inheritdoc /&gt;&nbsp; /// &lt;summary&gt;&nbsp; /// Resolves the site that best matches the given item.&nbsp; /// &lt;/summary&gt;&nbsp; /// &lt;param name="item"&gt;Requested item that we need to resolve matching site.&lt;/param&gt;&nbsp; /// &lt;returns&gt;&lt;/returns&gt;&nbsp; public SiteInfo ResolveSite(Item item)&nbsp; {&nbsp; Assert.ArgumentNotNull(item, "item");&nbsp; SiteInfo defaultSiteInfo = GetDefaultSiteInfo();&nbsp; if (SkipResolving(item))&nbsp; {&nbsp; return defaultSiteInfo;&nbsp; }&nbsp; return _realSiteResolver.ResolveSite(item) ?? defaultSiteInfo;&nbsp; } if SkipResolving is true, it takes site context from the default which is correct. However, it is false in this case and it goes to SiteResolver to resolve the site via item only. So there is a chance that the item is resolved to the wrong site since both CM and CD are pointing to the same Sitecore item path and who gets resolved is all dependent on the order of the sites. But why SkipResolving is false? // Sitecore.Links.UrlBuilders.Helpers.OptionsDecoratedSiteResolver&nbsp; using Sitecore.Data.Items;&nbsp; private bool SkipResolving(Item item)&nbsp; {&nbsp; if (!NoNeedToResolveSite(item) &amp;&amp; !OptionsSiteIsDifferentThanContextSite())&nbsp; {&nbsp; return ItemMatchesCurrentSite(item);&nbsp; }&nbsp; return true;&nbsp; }.....protected virtual bool ItemMatchesCurrentSite(Item item) { if (!Settings.Rendering.SiteResolvingMatchCurrentSite) { return false; } if (PathMatchesContextSite(item)) { return LanguageMatchesContextSite(item); } return false; }.....private bool LanguageMatchesContextSite(Item item) { string name = item.Language.Name; string language = ContextSite.Language; if (Settings.Rendering.SiteResolvingMatchCurrentLanguage) { return name.Equals(language, StringComparison.InvariantCultureIgnoreCase); } return true; } Out of all the code here, LanguageMatchesContextSite is false because of ContextSite.Language is empty but item.Language.Name is "en". It's all because "Language" field of site settings item(such as "/sitecore/content/tenant/Sites/siteA/Settings/Site Grouping/siteA-CM") is blank... After setting this field to "en", the issue is no longer presented. Must set "TargetHostname" if "Hostname" contains pipes or wildcards in SXA site settings This one was not causing a direct issue however could potentially be one. The GetTargetHostName method on SiteInfo is like below: // Sitecore.Web.SiteInfo /// &lt;summary&gt; /// Gets the name of the target host. /// &lt;/summary&gt; internal virtual string GetTargetHostName() { if (!string.IsNullOrEmpty(TargetHostName)) { return TargetHostName; } if (HostName.IndexOfAny(new char[2] { '*', '|' }) &lt; 0) { return HostName; } return string.Empty; } It's easy to see that if Hostname contains any wildcards or pipes and TargetHostName is blank, it would just return an empty string. Which would definitely cause issues somewhere on the site.]]></summary></entry><entry><title type="html">Hands on with Sitecore Experience Edge for Content Hub</title><link href="https://kezhan.info/2021/04/07/Hands-on-with-Sitecore-Experience-Edge-for-Content-Hub.html" rel="alternate" type="text/html" title="Hands on with Sitecore Experience Edge for Content Hub" /><published>2021-04-07T00:00:00+00:00</published><updated>2021-07-21T00:00:00+00:00</updated><id>https://kezhan.info/2021/04/07/Hands-on-with-Sitecore-Experience-Edge-for-Content-Hub</id><content type="html" xml:base="https://kezhan.info/2021/04/07/Hands-on-with-Sitecore-Experience-Edge-for-Content-Hub.html"><![CDATA[<div>One of the exciting news in the Sitecore world recently was the release of Sitecore Experience Edge for Content Hub (Experience Edge for XM is coming later this year, so will focus on Content Hub for now). If you haven't heard the news yet, I strongly recommend watching this <a target="_blank" href="https://youtu.be/T2BXGTWkLuw?t=664" rev="en_rl_none">Youtube </a>video to see it in real action. And here is the official <a target="_blank" href="https://docs.stylelabs.com/content/4.0.x/user-documentation/experience-edge/caas-intro.html" rev="en_rl_none">Experience Edge™ for Content Hub documentation</a> for a more thorough understanding. </div>
<div><br /></div>
<div>To get a hands-on demo experience with Sitecore Experience Edge for Content Hub, please talk to your Sitecore contacts for accessing the Content Hub Sandbox portal. The information below also applies to the Production setup, just ignore the parts related to sandboxes.</div>
<div><br /></div>
<ol><li><div>You should receive the instructions and accesses to create a sandbox Content Hub environment, just ensure the following when creating the sandbox</div></li><ol><li><div>Select version 4.0.0</div></li><li><div>Select "Content Publishing" license</div></li></ol><li><div>When CH sandbox is up and running, head to Settings-&gt;PublishingSettings and ensure "Publishing enabled" is ticked and Save.</div></li><li><div>Ensure that there are Content Collections and Content that are in the Final state (This is for viewing them in Delivery API)</div></li><li><div>Ensure that the Content Collections and Content have the green tick cloud icon that says "Published to delivery platform"</div></li><li><div>You can generate API tokens from Settings-&gt;API Keys or from the Content Collections</div></li><li><div>The URL to GraphQL IDE is&nbsp;<a target="_blank" href="https://[[Your demo instance hostname]]/api/graphql/preview/ide/" rev="en_rl_none">https://[[Your demo instance hostname]]/api/graphql/preview/ide/</a></div></li><li><div>Delivery API endpoint for demo containers is&nbsp;<a target="_blank" href="https://edge-beta.sitecorecloud.io/api/graphql/v1" rel="noopener noreferrer" rev="en_rl_none">https://</a><b><a target="_blank" href="https://edge-beta.sitecorecloud.io/api/graphql/v1" rel="noopener noreferrer" rev="en_rl_none">edge-beta.sitecorecloud.io</a></b><a target="_blank" href="https://edge-beta.sitecorecloud.io/api/graphql/v1" rel="noopener noreferrer" rev="en_rl_none">/api/graphql/v1</a>&nbsp;</div></li><li><div>Preview API endpoint for demo container is&nbsp;<a target="_blank" href="https://[[Your demo instance hostname]]/api/graphql/preview/v1" rev="en_rl_none">https://[[Your demo instance hostname]]/api/graphql/preview/v1</a></div></li></ol>
<div><br /></div>
<div style="text-align:start;">Note: For Production, the Delivery API endpoint is&nbsp;<a target="_blank" href="https://edge.sitecorecloud.io/api/graphql/v1" rel="noopener noreferrer" rev="en_rl_none">https://</a><b><a target="_blank" href="https://edge.sitecorecloud.io/api/graphql/v1" rel="noopener noreferrer" rev="en_rl_none">edge.sitecorecloud.io</a></b><a target="_blank" href="https://edge.sitecorecloud.io/api/graphql/v1" rel="noopener noreferrer" rev="en_rl_none">/api/graphql/v1</a></div>
<div style="text-align:start;"><br /></div>
<div style="text-align:start;">You can use Postman to test Delivery and Preview APIs too. Below is an export of a Postman collection:</div>
<div style="text-align:start;"><br /></div>
<div style="--en-codeblock:true;box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;"><div>{</div><div>	"info": {</div><div>		"_postman_id": "66f4925b-b6f0-4ed1-bea3-bdb09ba57045",</div><div>		"name": "Experience Edge For CH",</div><div>		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"</div><div>	},</div><div>	"item": [</div><div>		{</div><div>			"name": "Preview API",</div><div>			"request": {</div><div>				"method": "POST",</div><div>				"header": [</div><div>					{</div><div>						"key": "X-GQL-Token",</div><div>						"value": "",</div><div>						"type": "text"</div><div>					}</div><div>				],</div><div>				"body": {</div><div>					"mode": "graphql",</div><div>					"graphql": {</div><div>						"query": "{\r\n&nbsp; allM_ContentCollection {\r\n&nbsp; &nbsp; results {\r\n&nbsp; &nbsp; &nbsp; contentCollectionName\r\n&nbsp; &nbsp; &nbsp; id\r\n&nbsp; &nbsp; &nbsp; publishStatus\r\n&nbsp; &nbsp; &nbsp; contentCollectionToContent{\r\n&nbsp; &nbsp; &nbsp; &nbsp; results{\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_Name\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_PublicationDate\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; publishStatus\r\n&nbsp; &nbsp; &nbsp; &nbsp; }\r\n&nbsp; &nbsp; &nbsp; }\r\n&nbsp; &nbsp; }\r\n&nbsp; }\r\n}",</div><div>						"variables": ""</div><div>					},</div><div>					"options": {</div><div>						"raw": {</div><div>							"language": "json"</div><div>						}</div><div>					}</div><div>				},</div><div>				"url": {</div><div>					"raw": "https:///api/graphql/preview/v1",</div><div>					"protocol": "https",</div><div>					"host": [</div><div>						""</div><div>					],</div><div>					"path": [</div><div>						"api",</div><div>						"graphql",</div><div>						"preview",</div><div>						"v1"</div><div>					]</div><div>				}</div><div>			},</div><div>			"response": []</div><div>		},</div><div>		{</div><div>			"name": "Delivery API - Beta",</div><div>			"request": {</div><div>				"method": "POST",</div><div>				"header": [</div><div>					{</div><div>						"key": "X-GQL-Token",</div><div>						"value": "",</div><div>						"type": "text"</div><div>					}</div><div>				],</div><div>				"body": {</div><div>					"mode": "graphql",</div><div>					"graphql": {</div><div>						"query": "{\r\n&nbsp; allM_ContentCollection {\r\n&nbsp; &nbsp; results {\r\n&nbsp; &nbsp; &nbsp; contentCollectionName\r\n&nbsp; &nbsp; &nbsp; id\r\n&nbsp; &nbsp; &nbsp; publishStatus\r\n&nbsp; &nbsp; &nbsp; contentCollectionToContent{\r\n&nbsp; &nbsp; &nbsp; &nbsp; results{\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_Name\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_PublicationDate\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; publishStatus\r\n&nbsp; &nbsp; &nbsp; &nbsp; }\r\n&nbsp; &nbsp; &nbsp; }\r\n&nbsp; &nbsp; }\r\n&nbsp; }\r\n}",</div><div>						"variables": ""</div><div>					},</div><div>					"options": {</div><div>						"raw": {</div><div>							"language": "json"</div><div>						}</div><div>					}</div><div>				},</div><div>				"url": {</div><div>					"raw": "https://edge-beta.sitecorecloud.io/api/graphql/v1",</div><div>					"protocol": "https",</div><div>					"host": [</div><div>						"edge-beta",</div><div>						"sitecorecloud",</div><div>						"io"</div><div>					],</div><div>					"path": [</div><div>						"api",</div><div>						"graphql",</div><div>						"v1"</div><div>					]</div><div>				}</div><div>			},</div><div>			"response": []</div><div>		}</div><div>	]</div><div>}</div></div>
<div style="text-align:start;">&nbsp;</div>
<div>3 variables are:</div>
<ol><li><div>hostname</div></li><li><div>PreviewToken</div></li><li><div>DeliveryToken</div></li></ol>
<div><br /></div>
<div>Have fun!</div>]]></content><author><name></name></author><summary type="html"><![CDATA[One of the exciting news in the Sitecore world recently was the release of Sitecore Experience Edge for Content Hub (Experience Edge for XM is coming later this year, so will focus on Content Hub for now). If you haven't heard the news yet, I strongly recommend watching this Youtube video to see it in real action. And here is the official Experience Edge™ for Content Hub documentation for a more thorough understanding. To get a hands-on demo experience with Sitecore Experience Edge for Content Hub, please talk to your Sitecore contacts for accessing the Content Hub Sandbox portal. The information below also applies to the Production setup, just ignore the parts related to sandboxes. You should receive the instructions and accesses to create a sandbox Content Hub environment, just ensure the following when creating the sandboxSelect version 4.0.0Select "Content Publishing" licenseWhen CH sandbox is up and running, head to Settings-&gt;PublishingSettings and ensure "Publishing enabled" is ticked and Save.Ensure that there are Content Collections and Content that are in the Final state (This is for viewing them in Delivery API)Ensure that the Content Collections and Content have the green tick cloud icon that says "Published to delivery platform"You can generate API tokens from Settings-&gt;API Keys or from the Content CollectionsThe URL to GraphQL IDE is&nbsp;https://[[Your demo instance hostname]]/api/graphql/preview/ide/Delivery API endpoint for demo containers is&nbsp;https://edge-beta.sitecorecloud.io/api/graphql/v1&nbsp;Preview API endpoint for demo container is&nbsp;https://[[Your demo instance hostname]]/api/graphql/preview/v1 Note: For Production, the Delivery API endpoint is&nbsp;https://edge.sitecorecloud.io/api/graphql/v1 You can use Postman to test Delivery and Preview APIs too. Below is an export of a Postman collection: { "info": { "_postman_id": "66f4925b-b6f0-4ed1-bea3-bdb09ba57045", "name": "Experience Edge For CH", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "Preview API", "request": { "method": "POST", "header": [ { "key": "X-GQL-Token", "value": "", "type": "text" } ], "body": { "mode": "graphql", "graphql": { "query": "{\r\n&nbsp; allM_ContentCollection {\r\n&nbsp; &nbsp; results {\r\n&nbsp; &nbsp; &nbsp; contentCollectionName\r\n&nbsp; &nbsp; &nbsp; id\r\n&nbsp; &nbsp; &nbsp; publishStatus\r\n&nbsp; &nbsp; &nbsp; contentCollectionToContent{\r\n&nbsp; &nbsp; &nbsp; &nbsp; results{\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_Name\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_PublicationDate\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; publishStatus\r\n&nbsp; &nbsp; &nbsp; &nbsp; }\r\n&nbsp; &nbsp; &nbsp; }\r\n&nbsp; &nbsp; }\r\n&nbsp; }\r\n}", "variables": "" }, "options": { "raw": { "language": "json" } } }, "url": { "raw": "https:///api/graphql/preview/v1", "protocol": "https", "host": [ "" ], "path": [ "api", "graphql", "preview", "v1" ] } }, "response": [] }, { "name": "Delivery API - Beta", "request": { "method": "POST", "header": [ { "key": "X-GQL-Token", "value": "", "type": "text" } ], "body": { "mode": "graphql", "graphql": { "query": "{\r\n&nbsp; allM_ContentCollection {\r\n&nbsp; &nbsp; results {\r\n&nbsp; &nbsp; &nbsp; contentCollectionName\r\n&nbsp; &nbsp; &nbsp; id\r\n&nbsp; &nbsp; &nbsp; publishStatus\r\n&nbsp; &nbsp; &nbsp; contentCollectionToContent{\r\n&nbsp; &nbsp; &nbsp; &nbsp; results{\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_Name\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; content_PublicationDate\r\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; publishStatus\r\n&nbsp; &nbsp; &nbsp; &nbsp; }\r\n&nbsp; &nbsp; &nbsp; }\r\n&nbsp; &nbsp; }\r\n&nbsp; }\r\n}", "variables": "" }, "options": { "raw": { "language": "json" } } }, "url": { "raw": "https://edge-beta.sitecorecloud.io/api/graphql/v1", "protocol": "https", "host": [ "edge-beta", "sitecorecloud", "io" ], "path": [ "api", "graphql", "v1" ] } }, "response": [] } ]} &nbsp; 3 variables are: hostnamePreviewTokenDeliveryToken Have fun!]]></summary></entry><entry><title type="html">2021</title><link href="https://kezhan.info/2021/02/17/2021.html" rel="alternate" type="text/html" title="2021" /><published>2021-02-17T00:00:00+00:00</published><updated>2021-02-17T00:00:00+00:00</updated><id>https://kezhan.info/2021/02/17/2021</id><content type="html" xml:base="https://kezhan.info/2021/02/17/2021.html"><![CDATA[<div>The past year has been a weird year. It went by without much me feeling the time passing. Things happened around the world, but something also happened in our personal life.</div>
<div><br /></div>
<div>2020 was not too bad to be honest. We are positive people and of course 2021 will be even better.</div>]]></content><author><name></name></author><summary type="html"><![CDATA[The past year has been a weird year. It went by without much me feeling the time passing. Things happened around the world, but something also happened in our personal life. 2020 was not too bad to be honest. We are positive people and of course 2021 will be even better.]]></summary></entry><entry><title type="html">Sitecore Application Insights Setup Checklist in Azure App Services</title><link href="https://kezhan.info/2020/11/04/Sitecore-Application-Insights-Setup-Checklist-in-Azure-App-Services.html" rel="alternate" type="text/html" title="Sitecore Application Insights Setup Checklist in Azure App Services" /><published>2020-11-04T00:00:00+00:00</published><updated>2021-02-17T00:00:00+00:00</updated><id>https://kezhan.info/2020/11/04/Sitecore-Application-Insights-Setup-Checklist-in-Azure-App-Services</id><content type="html" xml:base="https://kezhan.info/2020/11/04/Sitecore-Application-Insights-Setup-Checklist-in-Azure-App-Services.html"><![CDATA[<div>Collected from different places. For my own references:</div>
<div><br /></div>
<div><b>Azure Application Insights</b></div>
<ol><li><div>Daily Volume Cap</div></li></ol>
<div><br /></div>
<div><b>Sitecore</b></div>
<ol><li><div>App_Config/ConnectionStrings.config</div></li><ol><li><div>appinsights.<b>instrumentationkey</b></div></li></ol><li><div>App_Config/Sitecore/Azure/Sitecore.Cloud.ApplicationInsights.config</div></li><li><div>App_Config/Sitecore/Azure/Sitecore.Cloud.ApplicationInsights.Counters.config</div></li><li><div>Web.config</div></li><ol><li><div>&lt;add key="<b>storeSitecoreCountersInApplicationInsights:define</b>" value="False" /&gt;</div></li><li><div>&lt;add key="<b>useApplicationInsights:define</b>" value="True" /&gt;</div></li><li><div>&lt;system.webServer&gt;</div><div>&nbsp; &lt;remove name="ApplicationInsightsWebTracking" /&gt;</div><div>&nbsp; &lt;add name="TelemetryCorrelationHttpModule" type="Microsoft.AspNet.TelemetryCorrelation.TelemetryCorrelationHttpModule, Microsoft.AspNet.TelemetryCorrelation" preCondition="integratedMode,managedHandler" /&gt;</div><div>&nbsp; &lt;add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" preCondition="managedHandler" /&gt;</div></li><li><div>&lt;system.web&gt;</div><div>&nbsp; &lt;trace enabled="false" requestLimit="50" pageOutput="false" traceMode="SortByTime" localOnly="true" /&gt;</div></li><li><div>&lt;system.diagnostics&gt;</div><div>&nbsp; &nbsp;&lt;trace autoflush="true" indentsize="0"&gt;</div><div>&nbsp; &nbsp; &nbsp; &lt;listeners&gt;</div><div>&nbsp; &nbsp; &nbsp; &nbsp; &lt;add name="myAppInsightsListener" type="Microsoft.ApplicationInsights.TraceListener.ApplicationInsightsTraceListener, Microsoft.ApplicationInsights.TraceListener" /&gt;</div><div>&nbsp; &nbsp; &nbsp; &lt;/listeners&gt;</div><div>&nbsp; &nbsp; &lt;/trace&gt;</div><div>&nbsp; &lt;/system.diagnostics&gt;</div></li></ol><li><div>ApplicationInsights.config</div></li></ol>
<div><br /></div>
<div><b>Sitecore Showconfig</b></div>
<ol><li><div>&lt;!-- SERVER ROLE The name for grouping metrics from instances by server role. Default value: Single --&gt;&lt;setting&nbsp;name="ApplicationInsights.<b>Role</b>"&nbsp;value="Single"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;</div></li><li><div>&lt;!-- TELEMETRY TAGS Tags that are included in telemetry data to identify the metrics from an instance. --&gt;&lt;setting&nbsp;name="ApplicationInsights.<b>Tag</b>"&nbsp;value=""&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;</div></li><li><div>&lt;!-- DEVELOPER MODE Enables developer mode in Application Insights TelemetryConfiguration. --&gt;&lt;setting&nbsp;name="ApplicationInsights.<b>DeveloperMode</b>"&nbsp;value="false"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;</div></li><li><div>&lt;pipelines&gt;&nbsp;</div><div>&nbsp;&nbsp; &nbsp;  &lt;initialize&gt;</div><div>&nbsp;&nbsp; &nbsp; &nbsp; &nbsp;  &lt;processor&nbsp;type="Sitecore.Cloud.ApplicationInsights.Logging.<b>RemoveSitecoreTraceListeners</b>, Sitecore.Cloud.ApplicationInsights"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;</div><div>&nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &lt;processor&nbsp;type="Sitecore.Cloud.ApplicationInsights.TelemertyInitializers.<b>InjectTelemertyInitializers</b>, Sitecore.Cloud.ApplicationInsights"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;</div><div>&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &lt;processor&nbsp;type="Sitecore.Cloud.ApplicationInsights.TelemertyInitializers.<b>AppInsightsInitializer</b>, Sitecore.Cloud.ApplicationInsights"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;</div></li></ol>
<div><br /></div>
<div><br /></div>
<div><b>Log files</b></div>
<ul><li><div>App_Data/logs/xxxx/azure.log.2020xxxx.0xxxx5.txt</div></li></ul>
<div><br /></div>]]></content><author><name></name></author><summary type="html"><![CDATA[Collected from different places. For my own references: Azure Application Insights Daily Volume Cap Sitecore App_Config/ConnectionStrings.configappinsights.instrumentationkeyApp_Config/Sitecore/Azure/Sitecore.Cloud.ApplicationInsights.configApp_Config/Sitecore/Azure/Sitecore.Cloud.ApplicationInsights.Counters.configWeb.config&lt;add key="storeSitecoreCountersInApplicationInsights:define" value="False" /&gt;&lt;add key="useApplicationInsights:define" value="True" /&gt;&lt;system.webServer&gt;&nbsp; &lt;remove name="ApplicationInsightsWebTracking" /&gt;&nbsp; &lt;add name="TelemetryCorrelationHttpModule" type="Microsoft.AspNet.TelemetryCorrelation.TelemetryCorrelationHttpModule, Microsoft.AspNet.TelemetryCorrelation" preCondition="integratedMode,managedHandler" /&gt;&nbsp; &lt;add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" preCondition="managedHandler" /&gt;&lt;system.web&gt;&nbsp; &lt;trace enabled="false" requestLimit="50" pageOutput="false" traceMode="SortByTime" localOnly="true" /&gt;&lt;system.diagnostics&gt;&nbsp; &nbsp;&lt;trace autoflush="true" indentsize="0"&gt;&nbsp; &nbsp; &nbsp; &lt;listeners&gt;&nbsp; &nbsp; &nbsp; &nbsp; &lt;add name="myAppInsightsListener" type="Microsoft.ApplicationInsights.TraceListener.ApplicationInsightsTraceListener, Microsoft.ApplicationInsights.TraceListener" /&gt;&nbsp; &nbsp; &nbsp; &lt;/listeners&gt;&nbsp; &nbsp; &lt;/trace&gt;&nbsp; &lt;/system.diagnostics&gt;ApplicationInsights.config Sitecore Showconfig &lt;!-- SERVER ROLE The name for grouping metrics from instances by server role. Default value: Single --&gt;&lt;setting&nbsp;name="ApplicationInsights.Role"&nbsp;value="Single"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;&lt;!-- TELEMETRY TAGS Tags that are included in telemetry data to identify the metrics from an instance. --&gt;&lt;setting&nbsp;name="ApplicationInsights.Tag"&nbsp;value=""&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;&lt;!-- DEVELOPER MODE Enables developer mode in Application Insights TelemetryConfiguration. --&gt;&lt;setting&nbsp;name="ApplicationInsights.DeveloperMode"&nbsp;value="false"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;&lt;pipelines&gt;&nbsp;&nbsp;&nbsp; &nbsp; &lt;initialize&gt;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &lt;processor&nbsp;type="Sitecore.Cloud.ApplicationInsights.Logging.RemoveSitecoreTraceListeners, Sitecore.Cloud.ApplicationInsights"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;&nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &lt;processor&nbsp;type="Sitecore.Cloud.ApplicationInsights.TelemertyInitializers.InjectTelemertyInitializers, Sitecore.Cloud.ApplicationInsights"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &lt;processor&nbsp;type="Sitecore.Cloud.ApplicationInsights.TelemertyInitializers.AppInsightsInitializer, Sitecore.Cloud.ApplicationInsights"&nbsp;patch:source="Sitecore.Cloud.ApplicationInsights.config"/&gt; Log files App_Data/logs/xxxx/azure.log.2020xxxx.0xxxx5.txt]]></summary></entry><entry><title type="html">Sitecore Installation Framework and Sitecore Versions Compatibility Table</title><link href="https://kezhan.info/2020/09/10/Sitecore-Installation-Framework-and-Sitecore-Versions-Compatibility-Table.html" rel="alternate" type="text/html" title="Sitecore Installation Framework and Sitecore Versions Compatibility Table" /><published>2020-09-10T00:00:00+00:00</published><updated>2020-09-11T00:00:00+00:00</updated><id>https://kezhan.info/2020/09/10/Sitecore-Installation-Framework-and-Sitecore-Versions-Compatibility-Table</id><content type="html" xml:base="https://kezhan.info/2020/09/10/Sitecore-Installation-Framework-and-Sitecore-Versions-Compatibility-Table.html"><![CDATA[<div><div>Probably not a problem that people typically have, but if you try to install multiple Sitecore versions (before Sitecore 10, which can be done using Docker now) on the same machine, you get weird errors due to incorrect&nbsp;Sitecore Installation Framework(SIF) version. So here is a table to help you to use the right version of SIF.</div><table style="border-collapse: collapse; min-width: 100%;"><colgroup><col style="width: 130px;" /><col style="width: 298px;" /></colgroup><tbody><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div><span style="font-weight: bold;">Sitecore</span></div></td><td style="width: 298px; padding: 8px; border: 1px solid;"><div><span style="font-weight: bold;">Sitecore Installation Framework</span></div></td></tr><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div>9.0.x</div></td><td style="width: 298px; padding: 8px; border: 1px solid;"><div>1.2.1</div></td></tr><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div>9.1</div></td><td style="width: 298px; padding: 8px; border: 1px solid;"><div>2.0.0</div></td></tr><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div>9.1.1</div></td><td style="width: 298px; padding: 8px; border: 1px solid;"><div>2.1.0 or later</div></td></tr><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div>9.2.0</div></td><td style="width: 298px; padding: 8px; border: 1px solid;"><div>2.1.0 or later</div></td></tr><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div>9.3.0</div></td><td style="width: 298px; padding: 8px; border: 1px solid;"><div>2.2.0</div></td></tr><tr><td style="width: 130px; padding: 8px; border: 1px solid;"><div>10.0.0</div></td><td style="width: 298px; padding: 8px; border: 1px solid;"><div>2.3.0</div></td></tr></tbody></table><div>And a cheatsheet for SIF.</div></div>
<div><br /></div>
<div>Add PowerShell repository for installing SIF</div>
<div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.15);-en-codeblock:true;"><div>Register-PSRepository -Name SitecoreGallery -SourceLocation https://sitecore.myget.org/F/sc-powershell/api/v2</div></div>
<div><br /></div>
<div>Install the latest version of SIF</div>
<div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.15);-en-codeblock:true;"><div>Install-Module SitecoreInstallFramework</div></div>
<div><br /></div>
<div>See all the versions of SIF installed</div>
<div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.15);-en-codeblock:true;"><div>Get-Module SitecoreInstallFramework –ListAvailable</div></div>
<div><br /></div>
<div>Install a specific version of SIF</div>
<div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.15);-en-codeblock:true;"><div>Install-Module -Name SitecoreInstallFramework -RequiredVersion x.x.x</div></div>
<div><br /></div>
<div>Use a particular version of SIF for installation - this is important when installing</div>
<div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.15);-en-codeblock:true;"><div>Import-Module -Name SitecoreInstallFramework -Force -RequiredVersion x.x.x</div></div>
<div><br /></div>
<div>There is an official Sitecore KB compatibility page containing the same information:</div>
<div><a target="_blank" href="https://kb.sitecore.net/articles/541788">https://kb.sitecore.net/articles/541788</a></div>
<div><br /></div>
<div>And finally, if you haven’t tried, I strongly recommend giving Docker a try for Sitecore 10 at least. It’s super easy!</div>]]></content><author><name></name></author><summary type="html"><![CDATA[Probably not a problem that people typically have, but if you try to install multiple Sitecore versions (before Sitecore 10, which can be done using Docker now) on the same machine, you get weird errors due to incorrect&nbsp;Sitecore Installation Framework(SIF) version. So here is a table to help you to use the right version of SIF.SitecoreSitecore Installation Framework9.0.x1.2.19.12.0.09.1.12.1.0 or later9.2.02.1.0 or later9.3.02.2.010.0.02.3.0And a cheatsheet for SIF. Add PowerShell repository for installing SIF Register-PSRepository -Name SitecoreGallery -SourceLocation https://sitecore.myget.org/F/sc-powershell/api/v2 Install the latest version of SIF Install-Module SitecoreInstallFramework See all the versions of SIF installed Get-Module SitecoreInstallFramework –ListAvailable Install a specific version of SIF Install-Module -Name SitecoreInstallFramework -RequiredVersion x.x.x Use a particular version of SIF for installation - this is important when installing Import-Module -Name SitecoreInstallFramework -Force -RequiredVersion x.x.x There is an official Sitecore KB compatibility page containing the same information: https://kb.sitecore.net/articles/541788 And finally, if you haven’t tried, I strongly recommend giving Docker a try for Sitecore 10 at least. It’s super easy!]]></summary></entry></feed>