<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Muhammad Ali Khan</title>
    <link>https://alikhan.tech/</link>
    <atom:link href="https://alikhan.tech/rss.xml" rel="self" type="application/rss+xml"/>
    <description>Muhammad Ali Khan is a full-stack engineer and Co-Founder &amp; CEO of MGN Ventures. He writes about Next.js, React, .NET, AI, and the systems that ship. 2× Sitecore MVP.</description>
    <language>en</language>
    <lastBuildDate>Tue, 27 Oct 2026 16:01:49 GMT</lastBuildDate>
    <item>
      <title>Making Sitecore Search Ready for AI Discovery — Attributes, Extractors, and Content That LLMs Can Cite</title>
      <link>https://alikhan.tech/blogs/2026/sitecore-search-ai-discovery</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2026/sitecore-search-ai-discovery</guid>
      <pubDate>Tue, 27 Oct 2026 03:00:00 GMT</pubDate>
      <description><![CDATA[Sitecore spent the back half of 2025 telling the market that discovery is moving from a list of blue links to answers. The 4 November 2025 SitecoreAI announcement said it directly: people meet a brand inside a feed or an AI-written summary, and the useful response is a governed…]]></description>
      <content:encoded><![CDATA[<p>Sitecore spent the back half of 2025 telling the market that discovery is moving from a list of blue links to answers. The 4 November 2025 SitecoreAI announcement said it directly: people meet a brand inside a feed or an AI-written summary, and the useful response is a governed message grounded in real content. Sitecore Search already had a feature for the on-site version of that problem. Questions and Answers, documented as a Sitecore Stream capability on top of your Search domain, generates answers from the content you indexed.</p>
<p>I spent this year watching Q&amp;A groups, and a couple of external answer engines, fail in the same way. The model was fine. The index document was a title, a thumbnail, and an empty body. This post is the attribute and extractor work I do before I turn generation on. It is also the part I am willing to call engineering guidance rather than a product feature: nothing in Sitecore&#39;s docs says &quot;make your pages citable by any LLM&quot;. The docs do say that an answer is only as good as the indexed items, and they return <code>document_id</code> and <code>context</code> with a Q&amp;A hit. Citation, in the product, means pointing at a document you extracted on purpose.</p>
<hr>
<h2 id="answers-are-an-index-feature">Answers are an index feature</h2>
<p>Sitecore&#39;s Q&amp;A documentation is specific. You create a question-and-answer group under Administration, Domain Settings, Feature Configuration. The group has a name, one or more sources, an entity, and up to five attributes. Those attributes are what generation is allowed to read. Textual relevance has to be enabled on each of them before you generate. You publish the feature configuration, open the Q&amp;A Browser, and run batch generation. The group gets an <code>rfk_id</code>. That id is what the widget request uses later.</p>
<p>If the source does not contain the fact, the API returns nothing. The same docs state that the feature supports English only. I re-read that page for multilingual sites, because a &quot;currently&quot; in the docs can move. As of the documentation I used for this post, I do not promise French or German answers from the product.</p>
<p>There is a second, quieter constraint. The visitor&#39;s keyphrase has to qualify as a question or a statement. A bag of keywords returns an empty Q&amp;A payload even when the index has the document. I have watched teams file this as an API bug after wiring Q&amp;A to the same box that serves keyword search. It is a documented qualification rule. Keyword search and answer generation are different requests. I issue them separately and I render an answer only when the response actually contains one.</p>
<p>Related questions use <code>questions.related_questions.limit</code>. An exact answer uses <code>questions.exact_answer</code>, and the example Sitecore publishes sets <code>query_types</code> to <code>[&quot;statement&quot;]</code>. You can send both in one widget item. The response includes the question, the answer, a <code>context</code> passage, and a <code>document_id</code>. That pair is the citation. I store it. I do not display an answer I cannot trace to a document in the group.</p>
<pre><code class="language-json">{
  &quot;widget&quot;: {
    &quot;items&quot;: [
      {
        &quot;entity&quot;: &quot;content&quot;,
        &quot;rfk_id&quot;: &quot;rfkid_product_docs&quot;,
        &quot;questions&quot;: {
          &quot;keyphrase&quot;: &quot;How long does a publish take to reach the CDN?&quot;,
          &quot;exact_answer&quot;: { &quot;query_types&quot;: [&quot;statement&quot;] },
          &quot;related_questions&quot;: { &quot;limit&quot;: 3 }
        }
      }
    ]
  }
}
</code></pre>
<p>The Cloud SDK&#39;s server package describes the same request as <code>QuestionsAnswersOptions</code>: a required <code>keyphrase</code>, optional <code>exactAnswer</code>, optional <code>relatedQuestions</code>, optional <code>rule</code>. I keep the Next.js call on the server. An API key that can read the domain does not belong in a browser bundle, and the answer is not a client-side concern until it is HTML I chose to render.</p>
<hr>
<h2 id="attributes-are-the-product">Attributes are the product</h2>
<p>Sitecore Search does not search your CMS. It searches index documents, and an index document is a set of attributes. The domain starts with a Content entity and a handful of default attributes. Everything else is one you create under Domain Settings, Attributes, and then extract.</p>
<p>The properties I set deliberately:</p>
<ul>
<li><strong>Attribute name.</strong> Alphanumeric, underscores, no spaces. This is the key in the extractor and the key in the API response when &quot;return in API response&quot; is on. I treat a rename as a migration. Widgets, sorting options, and Q&amp;A groups all bind to it.</li>
<li><strong>Data type.</strong> The type decides which features light up. A string can be a term facet. A number can be a histogram. A geo value is how I indexed coordinates back in 2024, and it is still a different type from a string that happens to look like <code>&quot;lat,lon&quot;</code>.</li>
<li><strong>Required.</strong> If the extractor cannot find a required attribute, Search does not index the item. I mark <code>id</code>, <code>title</code>, <code>url</code>, and <code>type</code> required. I do not mark a nice-to-have badge required, or a single missing badge drops the whole article.</li>
<li><strong>Default value.</strong> Useful for a constant like <code>type=article</code> when the template implies it. Dangerous for <code>description</code>, because a default sentence will be the sentence the model cites.</li>
<li><strong>Placement.</strong> Standard, or Locale when the domain is multilingual. Localized titles overwritten by a single shared value are a quiet relevance bug.</li>
</ul>
<p>Use-for-features comes after the attribute exists. Textual relevance is configured at the domain and then weighted on the global widget. Sitecore&#39;s own guidance is to keep a single attribute&#39;s weight at or below 5 so one field does not dominate. I weight <code>title</code> above <code>description</code>, and <code>description</code> above a long <code>content_text</code>, then I look at real queries before I touch the numbers again. Facets have their own settings: aggregation, and/or behavior, order, and a max. The docs warn that alphabetical sorting of a large value set and a max above 50 hurt performance, and that 100 is the ceiling. Q&amp;A does not require every attribute to be a facet. It requires the five you selected to carry text and to have textual relevance on.</p>
<p>A practical set for an article-like page:</p>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Type</th>
<th>Required</th>
<th>Why it exists</th>
</tr>
</thead>
<tbody><tr>
<td><code>id</code></td>
<td>string</td>
<td>yes</td>
<td>Stable identity. Q&amp;A returns this as <code>document_id</code>.</td>
</tr>
<tr>
<td><code>type</code></td>
<td>string</td>
<td>yes</td>
<td>Facet and a filter so Q&amp;A is not reading PDFs and landing pages as if they were the same.</td>
</tr>
<tr>
<td><code>title</code></td>
<td>string</td>
<td>yes</td>
<td>Result link and the highest textual weight.</td>
</tr>
<tr>
<td><code>url</code></td>
<td>string</td>
<td>yes</td>
<td>The human-visible citation target. Canonical, not the crawler&#39;s redirect hop.</td>
</tr>
<tr>
<td><code>description</code></td>
<td>string</td>
<td>no</td>
<td>Short summary. Often the best exact-answer source.</td>
</tr>
<tr>
<td><code>content_text</code></td>
<td>string</td>
<td>no</td>
<td>Body text with nav, cookie banners, and related-links stripped.</td>
</tr>
<tr>
<td><code>published_at</code></td>
<td>timestamp</td>
<td>no</td>
<td>Sort, and a date I am willing to show next to a citation.</td>
</tr>
<tr>
<td><code>language</code></td>
<td>string</td>
<td>yes on multi-locale domains</td>
<td>Keeps an English Q&amp;A group from reading the wrong locale.</td>
</tr>
</tbody></table>
<p>That table is my default, not a Sitecore schema. Your entity might be a product, a location, or a document. The rule that travels is: if a human would need the fact in order to trust a quote, extract it as its own attribute. Do not hope the model will recover it from a blob of chrome.</p>
<hr>
<h2 id="extractors-decide-whether-the-attribute-is-real">Extractors decide whether the attribute is real</h2>
<p>A source is either pull or push. Pull sources are the web crawler, the advanced web crawler, and the API crawler. Push sources take index documents through the Ingestion API. I use push when the content already lives in Sitecore and I can map fields myself. I use a crawler when the HTML is the source of truth, including pages the head application renders from Experience Edge.</p>
<p>The document extractor writes <code>key: value</code> pairs onto the index document. Sitecore&#39;s rule for non-JavaScript extractors is that they stop at the first match. A JavaScript extractor runs to the end. If the first <code>h1</code> on the page is the cookie-banner title, an XPath rule for <code>title</code> will lock onto it and never see the article heading. I either tighten the XPath to the main region or I switch that attribute to JavaScript and ignore the chrome on purpose.</p>
<p>For an advanced web crawler the documented extractor types are XPath, CSS, and JavaScript. An API crawler adds JSONPath. URLs-to-match on a tagger keep a blog extractor off the search page and the cart. Multiple taggers mean multiple sets of documents. I want one tagger per entity, with one rule per attribute, until I have a reason to split.</p>
<p>Validation shows the values a URL would produce and creates nothing. It also does not execute client-side JavaScript. A Next.js page that paints the article in the browser validates as an empty shell. Sitecore&#39;s documented workaround is the one I use: turn rendering on, set <code>MAX_URLS</code> low, run the crawler, and read <code>content_text</code> on the indexed document.</p>
<p>Sitemap triggers use <code>lastmod</code>. A sitemap that stamps every URL with the request time forces a full recrawl. Emit the real modification time.</p>
<hr>
<h2 id="the-citation-payload-i-want-in-every-document">The citation payload I want in every document</h2>
<p>This is the engineering guidance, labeled as such. Sitecore Q&amp;A will cite <code>document_id</code> and <code>context</code> because that is the response shape. A third-party answer engine that fetches the public URL will only cite what the HTML contains. I make those two views agree.</p>
<p>For each URL I am willing to have quoted, the HTML and the index document both expose:</p>
<ul>
<li>a canonical URL</li>
<li>one title</li>
<li>a description that a person could read aloud</li>
<li>a visible published or updated date</li>
<li>a stable id I also store in the index</li>
<li>body text that is present without executing a client-side bundle, where I can manage it</li>
</ul>
<p>The last bullet is the hard one on a pure client render. If the article body arrives only after hydration, a crawler that is not configured to render JavaScript indexes the shell, and some external fetchers never render at all. I do not have a Sitecore switch that fixes someone else&#39;s crawler. The lever I do have is server-rendered HTML for the pages we want discovered, plus an index document we control through a push source if the crawler is the wrong tool.</p>
<p>A push document for the same contract looks like this:</p>
<pre><code class="language-json">{
  &quot;document&quot;: {
    &quot;id&quot;: &quot;article-2026-10-27-cache-purge&quot;,
    &quot;fields&quot;: {
      &quot;type&quot;: &quot;article&quot;,
      &quot;title&quot;: &quot;How long a publish takes to reach the CDN&quot;,
      &quot;url&quot;: &quot;https://example.com/docs/publish-and-cdn&quot;,
      &quot;description&quot;: &quot;A publish updates Experience Edge first. The CDN keeps the previous HTML until revalidation or a cache-tag purge runs.&quot;,
      &quot;content_text&quot;: &quot;Publishing a Sitecore item updates the content delivery API. HTML cached at the CDN stays stale until the site purges the tag for that path.&quot;,
      &quot;published_at&quot;: &quot;2026-10-27T03:00:00Z&quot;,
      &quot;language&quot;: &quot;en&quot;
    }
  }
}
</code></pre>
<p><code>example.com</code> is a stand-in, not a client. The body is short on purpose. Q&amp;A was documented to read up to five attributes, not the entire CMS. I would rather extract two tight paragraphs than a 4,000-word page with the header menu concatenated in front of them. Concatenated chrome is how an answer quotes the login link.</p>
<hr>
<h2 id="a-checklist-i-run-before-the-first-batch-generation">A checklist I run before the first batch generation</h2>
<p>I keep this next to the source configuration. It is all documented product behavior, in the order that usually hurts.</p>
<ol>
<li>Create the attributes, including <code>url</code>, <code>title</code>, <code>description</code>, and <code>content_text</code>. Mark only the identity fields required.</li>
<li>Enable textual relevance on the attributes the Q&amp;A group will use. Weight them on the global widget. Stay at or under a weight of 5 on any single attribute until you have query evidence.</li>
<li>Extract them. For a web source, confirm whether the rule is XPath, CSS, or JavaScript, and remember that only the JavaScript rule runs past the first match.</li>
<li>Validate a handful of URLs. Then do a rendered crawl with a low <code>MAX_URLS</code> if the site is a JavaScript app. Open an indexed document and read <code>content_text</code> with your eyes.</li>
<li>Create the Q&amp;A group, pick the source and the entity, add no more than five attributes, publish feature configuration, and run batch generation. Note the <code>rfk_id</code>.</li>
<li>Call the widget with a full question and with a two-word keyword. The question should be eligible for an answer. The keyword should come back empty. If the keyword returns an answer, I do not trust the integration yet and I re-read the qualification note.</li>
<li>Render <code>answer</code> together with <code>document_id</code> and the <code>url</code> of that document. An answer with no link is not a citation. It is a paragraph I cannot defend.</li>
</ol>
<p>The product documentation I am using limits Q&amp;A to English. I still extract <code>language</code> for keyword search, and I do not point the English group at every locale&#39;s source.</p>
<p>A clean index does not guarantee that some other assistant will quote the site. That depends on crawlers Sitecore does not operate. Sitecore Search can ground an answer only in attributes you extracted, and the Q&amp;A API returns the document identity and a context passage when it has one. A Next.js page that hides its text in the client feeds both an empty document.</p>
<p>Name the attributes. Extract the words a person would quote. Prove it with a small crawl. Then turn generation on.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Agentic Studio 101 for Developers — Where Agents Stop and Your Code Still Matters</title>
      <link>https://alikhan.tech/blogs/2026/agentic-studio-101-for-developers</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2026/agentic-studio-101-for-developers</guid>
      <pubDate>Mon, 26 Oct 2026 03:00:00 GMT</pubDate>
      <description><![CDATA[Agentic Studio is the part of SitecoreAI that demos well and integrates quietly. Sitecore introduced it on 4 November 2025 with the SitecoreAI announcement: a workspace where marketers and agents plan, create, and personalize, with 20 prebuilt agents and a visual builder for…]]></description>
      <content:encoded><![CDATA[<p>Agentic Studio is the part of SitecoreAI that demos well and integrates quietly. Sitecore introduced it on 4 November 2025 with the SitecoreAI announcement: a workspace where marketers and agents plan, create, and personalize, with 20 prebuilt agents and a visual builder for custom ones. A year later I am usually in the room as the person who has to explain why the agent&#39;s &quot;successful&quot; run did not change the website.</p>
<p>The explanation is almost always the same. The agent did what its tools allow, inside the caller&#39;s permissions, and wrote Sitecore items. The Next.js app renders only the renderings it was built with, through the fields those renderings read. Agentic Studio does not compile that app. This is the 101 I give developers who are about to be asked to &quot;just add an agent&quot; to an existing SitecoreAI implementation.</p>
<hr>
<h2 id="two-kinds-of-custom-agent-and-only-one-of-them-is-a-workflow">Two kinds of custom agent, and only one of them is a workflow</h2>
<p>Sitecore&#39;s Agentic Studio docs split custom agents by shape.</p>
<p>A <strong>standard agent</strong> is the chat-style, task-driven one. You reach for it when the user is still deciding what they want, iterating on a brief, or asking for a draft they will edit by hand. It is the closer cousin of the Stream optimize-content panel I wrote about in 2025, except it sits in Agentic Studio and can call SitecoreAI tools rather than only rewriting the field in front of the author.</p>
<p>A <strong>workflow agent</strong> is for a sequence you can draw. Sitecore recommends it when the outcome should be consistent: a fixed order of steps, approvals, and structured outputs. The workflow editor opens on five tabs. Overview holds the name, the description, and the run-page copy (&quot;what is it for&quot;, &quot;how it works&quot;) plus the inputs the user must fill in. Parameters, schemas, and HTML templates shape what the agent accepts and how a result is presented. The Workflow tab is the sequence itself.</p>
<p>You need a Builder license to create either kind. I confirm that license before I sit down with a marketer to sketch steps. Duplicating a prebuilt agent is the documented shortcut when the change is small, and an exported JSON definition is a reasonable thing to keep in source control next to the rendering contract it depends on. I do not treat that JSON as a substitute for the Next.js component. It is configuration. The component is code.</p>
<p>A fixed sequence belongs in a workflow agent. A conversation that is still finding the brief belongs in a standard agent.</p>
<hr>
<h2 id="the-tools-are-sitecore-operations-not-react-components">The tools are Sitecore operations, not React components</h2>
<p>Workflow steps do their Sitecore work through the Invoke Tool action. The configuration is three fields: the tool, a Parameters JSON object, and an output variable that stores the response. A successful response carries <code>success: true</code> and a <code>data</code> object. A failure carries <code>success: false</code> and an <code>error</code> string. Some tools nest further, which is why the second step in a flow so often breaks.</p>
<p>Sitecore&#39;s own walkthrough chains Search Pages into Get Page Content. Search Pages takes <code>siteName</code>, <code>searchText</code>, <code>language</code>, and <code>pageSize</code>, and each hit includes an <code>itemId</code>. Get Page Content wants that id as <code>pageId</code>. The workflow expression that connects them is not &quot;pass the previous output&quot;. You type <code>@</code> in the Parameters editor, pick the upstream step, and then edit the path until it resolves to the id string alone. A path that resolves to the whole tool payload is a type error the tool will not forgive. The expression in Sitecore&#39;s walkthrough names the upstream step and ends at <code>itemId</code>. The step id is generated. The lesson I care about is that last segment: the id, not the envelope.</p>
<p>I redraw that flow for our sites with our site name and a rendering we actually ship:</p>
<pre><code class="language-json">{
  &quot;siteName&quot;: &quot;marketing&quot;,
  &quot;searchText&quot;: &quot;product overview&quot;,
  &quot;language&quot;: &quot;en&quot;,
  &quot;pageSize&quot;: 1
}
</code></pre>
<p>The following step may only receive the id:</p>
<pre><code class="language-json">{
  &quot;pageId&quot;: &quot;{{@&lt;generatedStepId&gt;:Find product page.data...itemId}}&quot;
}
</code></pre>
<p>I left the middle of the path as <code>data...</code> on purpose. Sitecore&#39;s docs say the exact path varies by tool and that you should inspect the previous step&#39;s output when a later step fails. Copying a path from a blog, including this one, is how you bind to <code>data.data.data.1</code> on a tool that returned <code>data.results[0]</code>. Open the step log. Point the expression at the id. Then lock the flow.</p>
<p>The Invoke Tool catalog is wide. The docs group it into site discovery, page management, components, assets, personalization, brand kits, briefs, and experiments, and there is a <code>web_search</code> tool whose sample parameter is a <code>query</code> string. Web search is the one I flag in review. It can pull public pages into a prompt. It must not be the source of record for a claim we then publish on our own site. Our pages, our brand kit, and our indexed content are the sources I want in the flow. The public web is research, and a person should read it before it becomes a field value.</p>
<hr>
<h2 id="the-same-operations-exist-as-an-api-and-as-an-mcp-server">The same operations exist as an API and as an MCP server</h2>
<p>Agentic Studio is the UI. The Agent API is the surface underneath it, and it is documented for developers who are not clicking the workflow editor. It is a REST API, called in the production environment, authorized with an automation-client JWT or with an OAuth app. An Organization Admin or Owner creates the automation client under SitecoreAI Deploy, and the client secret is shown once. I store that secret the way I store any other production credential. I do not paste it into a workflow screenshot.</p>
<p>The documented object areas match the tools: sites, pages, content items, components and datasources, assets, environments and languages, personalization variants, jobs, brand kits, brand context, briefs, experiments, and flow definitions for A/B/n tests. Every call still passes through Sitecore&#39;s security and approval rules. The detail I make sure our team hears is the job id. If an agent writes the wrong thing, the job id is the handle Sitecore documents for reverting that operation. &quot;The AI did it&quot; is not an audit trail. The job id is.</p>
<p>The Marketer MCP server is the natural-language front door onto those same endpoints. Sitecore documents it as an MCP server that MCP clients such as Claude Desktop and Cursor can use to create pages, place components, and update content. The tool reference says the actions remain subject to the user&#39;s permissions and to whatever review, approval, versioning, and publishing workflow the site already has. I like that sentence. It means an MCP client is not a backdoor around the workflow we spent last year putting on the site. It also means a developer with publish rights, pointed at production, can do real damage in one prompt. The automation client should be scoped the way we scope a CI credential, and production prompts belong to people who already understand the site.</p>
<p>I do not wrap the Agent API in a second undocumented facade inside the Next.js app &quot;so the frontend can call agents&quot;. The rendering app&#39;s job is to read published layout data and draw it. Agent calls belong in a server-side integration with a credential that never reaches the browser, or they belong in Agentic Studio itself. A <code>NEXT_PUBLIC_</code> secret for the automation client would be a defect, not a shortcut.</p>
<hr>
<h2 id="the-contract-i-write-down-before-anyone-opens-the-workflow-editor">The contract I write down before anyone opens the workflow editor</h2>
<p>This is the page I want in the repo. It is short because the agent will not read our component source unless we hand it the constraints.</p>
<pre><code class="language-ts">export const productHeroContract = {
  renderingName: &#39;ProductHero&#39;,
  templateId: &#39;B6C3E0A1-1111-2222-3333-444455556666&#39;,
  fields: {
    heading: { type: &#39;single-line&#39;, maxLength: 80 },
    summary: { type: &#39;multi-line&#39;, maxLength: 240 },
    image: { type: &#39;image&#39;, required: true },
    cta: { type: &#39;general-link&#39; },
  },
} as const;
</code></pre>
<p>The GUID above is a placeholder. Yours comes from the template item. The rules I enforce around it:</p>
<ul>
<li>The rendering name matches the key in the Content SDK component map, character for character.</li>
<li>The fields listed are the fields the React component reads. Extra fields are allowed only if a developer has agreed to ignore them on purpose.</li>
<li><code>maxLength</code> is a product decision we also enforce in the agent prompt and, where Sitecore validation exists, on the template. The agent will otherwise write a hero headline the length of a blog lede.</li>
<li>Required image means the flow has to pick an asset from the library or stop. I would rather the step fail than publish a hero with a broken image field.</li>
<li>Links use the site&#39;s link format. A bare <code>https://</code> string stuffed into a general link field is a classic &quot;the tool returned success and the component rendered nothing&quot; bug.</li>
</ul>
<p>Schemas and HTML templates in the workflow agent are how Sitecore lets you shape the agent&#39;s own output. They are useful for a brief, a tweet, or a summary the user will copy. They are not a second design system. When the output is supposed to land on a page, I prefer Invoke Tool against a known template over an HTML blob the page builder cannot edit properly later.</p>
<hr>
<h2 id="where-i-still-want-application-code">Where I still want application code</h2>
<p><strong>Rendering and design.</strong> Layout, responsive behavior, accessibility, and the design system live in the Next.js app. An agent that emits a one-off HTML fragment for a campaign landing page will not pick up the next brand refresh. Pages composed from existing renderings will.</p>
<p><strong>Authoring mode.</strong> Content SDK exposes <code>page.mode</code>. Chrome, placeholders, and field metadata are the SDK&#39;s job. Our job is to keep custom components from hiding buttons, trapping focus, or rendering a different DOM tree in editing mode than they do on the live site. If the trees diverge, the page builder and the published page drift apart and authors stop trusting both.</p>
<p><strong>Personalization variants that have logic.</strong> SitecoreAI can create variants and experiments through the Agent API. The variant is still content. If a variant depends on a claim code, a pricing rule, or a feature flag, that rule stays in code or in the personalization engine we already operate. I do not bury it in a system prompt.</p>
<p><strong>Publishing and cache.</strong> A flow that creates a page has not published it unless a publish step ran, and a publish has not updated the CDN unless our revalidation ran. I keep that wiring in the application and in the existing publish hook. Agentic Studio does not know that Netlify, or any other host, keyed the HTML on a cache tag.</p>
<p><strong>Secrets, retries, and idempotency.</strong> Workflow tools can fail halfway. I want a flow that searches for the page it might already have created before it creates another one. That is ordinary integration hygiene. The visual editor will not add it unless someone who has been paged for duplicate items insists.</p>
<hr>
<h2 id="a-review-i-run-the-first-time-a-flow-touches-a-real-site">A review I run the first time a flow touches a real site</h2>
<p>I ask for a non-production site and a content tree we can delete. Then I walk the flow once and keep the notes.</p>
<ol>
<li>Who can run it, and does that user have publish rights? If they do, say so in the run-page copy. &quot;How it works&quot; is not the place to be vague.</li>
<li>Does every Invoke Tool parameter resolve to the scalar the next tool documents, especially ids?</li>
<li>Does the created item use a template our component map renders? Open the page builder. If the rendering is unknown, the flow is ahead of the repository.</li>
<li>Is there a job id in the run log, and has someone actually reverted a test write with it? I want that rehearsal before the first marketing demo.</li>
<li>Did a publish happen, and did the head app show the new field values without a redeploy? If a new rendering was required, the answer is no, and the flow should not pretend otherwise.</li>
<li>Are brand-kit and language inputs explicit? Sitecore&#39;s November 2025 examples, Berkeley Homes and AFL among them, were about contextually aware content aimed at the right audience and channel. Those were Sitecore&#39;s customers in the press release, not a template you can copy. On our sites the brand kit and the <code>language</code> parameter have to be chosen, not assumed.</li>
</ol>
<hr>
<h2 id="what-i-tell-a-developer-who-is-new-to-the-studio">What I tell a developer who is new to the studio</h2>
<p>Learn the five tabs and one chain: Search Pages, then Get Page Content, then a write or a Generate Content step you can read. Learn the <code>@</code> path rule. Learn where the automation client is stored. Then stop, and go read the component that will render the result.</p>
<p>The prebuilt agents from November 2025 are allowed to be useful without a pull request. Campaign drafts, migration assistance, test ideas: that is the point of shipping twenty of them. The moment a flow creates a rendering, a template, or a field, it has crossed into the application, and the application changes in git. That line is the whole 101. Agents are very good at operating Sitecore. They are not the Sitecore implementation.</p>
]]></content:encoded>
    </item>
    <item>
      <title>SitecoreAI After XM Cloud — What Actually Changes for a Next.js Implementation Team</title>
      <link>https://alikhan.tech/blogs/2026/sitecoreai-after-xm-cloud-nextjs</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2026/sitecoreai-after-xm-cloud-nextjs</guid>
      <pubDate>Sun, 25 Oct 2026 03:00:00 GMT</pubDate>
      <description><![CDATA[Sitecore announced SitecoreAI on 4 November 2025. The press release was clear about the commercial story: SitecoreAI is the evolution of XM Cloud, current XM Cloud customers do not run a content migration, data stays in place, and Agentic Studio shows up on the same tenant. I…]]></description>
      <content:encoded><![CDATA[<p>Sitecore announced SitecoreAI on 4 November 2025. The press release was clear about the commercial story: SitecoreAI is the evolution of XM Cloud, current XM Cloud customers do not run a content migration, data stays in place, and Agentic Studio shows up on the same tenant. I have spent the year since that announcement on Next.js implementation work, and the question I still get in kickoff calls is the same one. If the upgrade is seamless, why does the head application feel like a project?</p>
<p>The honest answer is that two upgrades got collapsed into one sentence. The SaaS content tenant moved forward without a replatform. The JavaScript stack that renders that tenant has its own lifecycle, and that lifecycle has been moving since the week of the announcement. This post is the map I use with a Next.js team in October 2026, looking back at what was public in November 2025 and what the developer documentation says now.</p>
<hr>
<h2 id="what-november-2025-actually-shipped">What November 2025 actually shipped</h2>
<p>The 4 November 2025 announcement, published from London, described SitecoreAI as a composable SaaS platform built on XM Cloud and hosted on Microsoft Azure. It unifies content management, customer data, personalization, and search. The new workspace is Agentic Studio. Sitecore said it launched with 20 prebuilt agents covering work from campaign planning through content migration, production, and testing, plus visual tools so marketers and agency partners can design their own agents and flows. Agentic Flows orchestrate multi-step work from briefing and experimentation through publishing. Spaces are the collaboration surface between people and those agents.</p>
<p>The same release named the upgrade path for existing XM Cloud customers: no migration, continuity of data, and access to Agentic Studio and the prebuilt agents. I take that sentence literally. Items, templates, sites, media, and the Experience Edge content the site already reads are not thrown away because the product name changed. When a stakeholder asks me to budget a &quot;SitecoreAI migration&quot; for a healthy XM Cloud implementation, I split the estimate. Tenant continuity is the vendor&#39;s claim. Head-application work is ours, and it is a different line item.</p>
<p>Sitecore also framed the product around discovery that happens outside the website: feeds, summaries, and moments where the brand has to show up with a governed message. That framing matters for implementation teams because it pulls Search, brand kits, and the rendering app into one conversation. It does not, by itself, change a <code>getStaticProps</code> function.</p>
<p>A few days before the announcement, the Sitecore developer changelog recorded two SDK facts that are easy to miss if you only read the press release. On 29 October 2025 Sitecore published an SDK lifecycle for XM Cloud: Content SDK 1.x became the actively supported SDK, and JSS support on XM Cloud moved onto a phase-out path. On 30 October 2025 Content SDK 1.2 added Next.js App Router support and shipped it as a beta. Server components, streaming, nested layouts, and fetching layout data from server components were in that beta. Sitecore said the beta was supported for teams who wanted to try it, and that the shape could still change. I treated &quot;try it&quot; as the operative instruction in late 2025. I still treat a router migration as its own project in 2026.</p>
<hr>
<h2 id="what-did-not-change-in-the-nextjs-app-on-day-one">What did not change in the Next.js app on day one</h2>
<p>The websites I work on still render Sitecore content through a head application. Pages are still compositions of placeholders and components. Field values still arrive as layout data or as a GraphQL result. Experience Edge is still the delivery API in front of published content. A page that was fast in October 2025 did not become fast because Agentic Studio appeared in the portal, and a page that over-fetched GraphQL did not heal itself either.</p>
<p>Authors still publish. Workflows, if you had them, still gate what Edge will serve. I wrote earlier in 2025 about Stream&#39;s optimize-content actions inside the page editor, and about cache tag purge when a publish has to show up on Netlify. Those problems are still implementation problems. SitecoreAI gives authors more ways to draft and to run a governed flow. The CDN in front of the Next.js app does not learn about that flow unless we invalidate it.</p>
<p>Component code remains the contract for anything an agent is allowed to create. If the hero component reads <code>heading</code>, <code>summary</code>, and <code>image</code>, an agent that writes a fourth field into a datasource does not get a new visual. It gets a value the component ignores. I have watched teams discover this the expensive way: a flow &quot;succeeds&quot; in Agentic Studio, the page item exists, and the site renders an empty region because the rendering item or the component map was never part of the flow.</p>
<p>Search stays a configured product. SitecoreAI&#39;s story includes search, and the Questions and Answers capability in Sitecore Search is a real, documented feature. Index attributes, document extractors, and widget <code>rfkId</code>s do not reconfigure themselves when the portal is rebranded. I cover that work in the posts later this week. For this one, the point is narrower. Announcement week was not a Search cutover.</p>
<hr>
<h2 id="what-did-change-for-the-people-who-maintain-the-head">What did change for the people who maintain the head</h2>
<p>The SDK lifecycle is the change I plan against. JSS carried XM Cloud sites for years, including the multisite Next.js apps most of us still have in production. Sitecore&#39;s own position, written into the October 2025 lifecycle note and repeated in the Content SDK upgrade guides, is that one JavaScript SDK should not keep serving two diverging runtimes. Content SDK is the SDK for SitecoreAI. It drops Experience Editor chromes, generates a component map instead of the old component builder, and funnels data access through <code>SitecoreClient</code>.</p>
<p>By October 2026 the upgrade guide I hand a team is the Content SDK 2.1 guide for a JSS 22.0 Next.js app. That guide expects Next.js 16, React 19, <code>sitecore.config.ts</code>, <code>sitecore.cli.config.ts</code>, and a <code>defineProxy</code> helper, because Next.js 16 runs <code>proxy.ts</code>. Content SDK 1.5 and earlier called the same idea <code>defineMiddleware</code>. Match the guide to the package version you install, and trust the <code>create-content-sdk-app</code> scaffold when a checklist still says only <code>middleware.ts</code>.</p>
<p>Editing changed in a way authors notice before developers do. Content SDK does not support the Experience Editor, and it does not support preview opened from the Content Editor. Preview and visual editing go through the SitecoreAI page builder, using metadata on the fields. <code>field.editable</code> is gone. Field components read <code>field.metadata</code>. <code>pageState</code>, <code>pageEditing</code>, and <code>renderingType</code> on the layout context fold into <code>page.mode</code>, with flags such as <code>isEditing</code>, <code>isPreview</code>, <code>isNormal</code>, and <code>isDesignLibrary</code>. Any component that branched on the old context properties still compiles only if nobody deleted the property access. Once the import moves to <code>@sitecore-content-sdk/nextjs</code>, those branches fail, or worse, they silently stop wrapping the authoring chrome.</p>
<p>Environment names moved with the SDK. The 2.1 guide renames <code>SITECORE_API_KEY</code> to <code>NEXT_PUBLIC_SITECORE_API_KEY</code>, <code>SITECORE_API_HOST</code> to <code>NEXT_PUBLIC_SITECORE_API_HOST</code>, <code>SITECORE_SITE_NAME</code> to <code>NEXT_PUBLIC_DEFAULT_SITE_NAME</code>, <code>DEFAULT_LANGUAGE</code> to <code>NEXT_PUBLIC_DEFAULT_LANGUAGE</code>, and <code>JSS_EDITING_SECRET</code> to <code>SITECORE_EDITING_SECRET</code>. <code>SITECORE_EDGE_URL</code> is replaced by <code>NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME</code> on Next.js so the client bundle and the server agree. <code>DISABLE_SSG_FETCH</code> becomes <code>GENERATE_STATIC_PATHS</code>, and the meaning is inverted. I have seen a pipeline &quot;succeed&quot; with an empty static path list because someone copied <code>true</code> from the old variable into the new one.</p>
<p>Logging into Agentic Studio in November 2025 did not require any of it. Picking up the SDK Sitecore is supporting now does.</p>
<hr>
<h2 id="how-i-separate-the-work">How I separate the work</h2>
<p>I keep four buckets on the board. Mixing them is how a two-week authoring rollout becomes a quarter-long rewrite.</p>
<ol>
<li><p><strong>Tenant continuity.</strong> Confirm the organization is on SitecoreAI, Agentic Studio is visible to the roles that need it, and existing sites still resolve. This is mostly an admin and smoke-test exercise. Sitecore&#39;s November 2025 statement was that XM Cloud customers do not migrate content to get there.</p>
</li>
<li><p><strong>Authoring and agents.</strong> Brand kits, prebuilt agents, and any custom workflow agent. Custom agents need a Builder license, which is documented on the create-a-workflow-agent page. This bucket changes the portal. It should only create pages and content that the current component set already understands.</p>
</li>
<li><p><strong>The rendering app.</strong> Content SDK, on the version you can support, Pages Router first if that is what production runs. App Router support shipped as a beta in Content SDK 1.2 at the end of October 2025. I still schedule that router move separately from the package swap.</p>
</li>
<li><p><strong>Discovery.</strong> Sitecore Search attributes, extractors, and the Questions and Answers group if you want answers grounded in your index. That work pays off the &quot;world beyond the website&quot; line in the press release. It is not a side effect of installing Content SDK.</p>
</li>
</ol>
<hr>
<h2 id="a-small-guard-i-add-in-the-nextjs-app">A small guard I add in the Next.js app</h2>
<p>Agents and the page builder both end up calling the same renderings. The guard I want is boring. It asks two questions: are we in an authoring mode the page builder understands, and is every rendering name present in the component map the build generated?</p>
<pre><code class="language-ts">type PageMode = {
  isEditing?: boolean;
  isPreview?: boolean;
  isNormal?: boolean;
  isDesignLibrary?: boolean;
};

export function isAuthoringMode(mode: PageMode | undefined): boolean {
  return Boolean(mode?.isEditing || mode?.isPreview || mode?.isDesignLibrary);
}

export function assertKnownRendering(
  componentName: string,
  componentMap: Record&lt;string, unknown&gt;
): void {
  if (!componentMap[componentName]) {
    throw new Error(
      `Rendering &quot;${componentName}&quot; is not in the Content SDK component map. ` +
        `An agent or the page builder can place it only after the Next.js app registers it.`
    );
  }
}
</code></pre>
<p><code>page.mode</code> is the Content SDK shape. The throw is my choice. The SDK&#39;s usual behavior for an unknown rendering is to render a fallback, which is friendlier in production and useless in a migration branch. I fail the authoring build so a new agent flow cannot ship a rendering the repository has never heard of.</p>
<p>The matching config check is about Edge, not about agents. After the environment rename, a Next.js app should resolve the platform host from the public variable and fall back to Sitecore&#39;s default only when nobody has set one:</p>
<pre><code class="language-ts">const edgeUrl =
  process.env.NEXT_PUBLIC_SITECORE_EDGE_PLATFORM_HOSTNAME ||
  &#39;https://edge-platform.sitecorecloud.io&#39;;
</code></pre>
<p>That default is the one the Content SDK 2.1 guide documents. If your tenant uses a custom Edge hostname, the public variable has to be present at build time. Next.js inlines <code>NEXT_PUBLIC_*</code> then. A runtime-only secret with the old <code>SITECORE_EDGE_URL</code> name will not reach the client, and preview will look randomly broken depending on which side issued the request.</p>
<hr>
<h2 id="what-i-refuse-to-put-in-the-same-sprint">What I refuse to put in the same sprint</h2>
<p>I do not schedule a head rewrite because the product brochure changed. The November 2025 promise to XM Cloud customers was continuity. Spending that continuity on a from-scratch App Router app throws away the part that was actually seamless.</p>
<p>I do not let a workflow agent create templates. The Next.js component and the GraphQL selection are compiled. An agent can fill fields, place a rendering that already exists, and start an experiment. The Agent API documents job ids so a bad write can be reverted. Search Q&amp;A stays a separate piece of work: generation only sees attributes you extracted, and extractor validation does not execute client-side JavaScript.</p>
<hr>
<h2 id="a-sequence-that-has-survived-contact-with-a-real-backlog">A sequence that has survived contact with a real backlog</h2>
<p>This is the order I recommend when the site is already on XM Cloud and SitecoreAI is in the portal. It is an implementation sequence, not a Sitecore-mandated program.</p>
<ol>
<li>Smoke-test the existing site, the page builder, and one prebuilt agent on a non-production site. Confirm a publish still reaches Edge and that cache purge still fires.</li>
<li>Inventory JSS: package names, <code>temp/config</code>, page-props plugins, and every read of <code>field.editable</code>.</li>
<li>Pick one Content SDK version and read only that upgrade guide. In October 2026, for a JSS 22 app that can take Next.js 16, that is the 2.1 guide. Stay on the Pages Router for the first merge.</li>
<li>Generate the component map in CI with <code>sitecore-tools</code> before <code>next build</code>.</li>
<li>Design custom agents after that, using rendering and field names from the inventory, a Builder license, and a rollback drill with the Agent API job id.</li>
<li>Schedule App Router and Search Q&amp;A as follow-on work with their own test plans.</li>
</ol>
<hr>
<h2 id="documentation-i-kept-open">Documentation I kept open</h2>
<ul>
<li>Sitecore&#39;s 4 November 2025 press release, &quot;Sitecore Unveils SitecoreAI&quot;.</li>
<li>Sitecore developer changelog, 29 October 2025, &quot;New SDK lifecycle introduced for XM Cloud&quot;.</li>
<li>Sitecore developer changelog, 30 October 2025, &quot;Content SDK v1.2 — App Router support&quot;.</li>
<li>Sitecore documentation, &quot;Upgrade JSS 22.0 Next.js apps to Content SDK 2.1&quot;, which is the guide I am using in October 2026.</li>
<li>The Agentic Studio and Agent API docs, for the boundary between a flow and the rendering app. The next post goes into that boundary in detail.</li>
</ul>
<p>The platform move was real in November 2025, and for an XM Cloud customer it was mostly a portal change. The Next.js change is real too. It shows up when you leave JSS, when you follow page builder metadata, and when you stop assuming an agent understands a component you have not registered. Those are the items I put on the board. The product rename is not one of them.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Next.js ISR + Netlify Cache Tag Purge for Sitecore Site Pages</title>
      <link>https://alikhan.tech/blogs/2025/netlify-cache-tag-purge-isr</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/netlify-cache-tag-purge-isr</guid>
      <pubDate>Mon, 10 Nov 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[We recently deployed our Sitecore Next.js site on Netlify and ran into a frustrating caching problem. Even after revalidating pages, they stayed cached. The standard Next.js ISR revalidation just wasn't working reliably on Netlify's edge network, which meant content authors…]]></description>
      <content:encoded><![CDATA[<p>We recently deployed our Sitecore Next.js site on Netlify and ran into a frustrating caching problem. Even after revalidating pages, they stayed cached. The standard Next.js ISR revalidation just wasn&#39;t working reliably on Netlify&#39;s edge network, which meant content authors would publish updates and they wouldn&#39;t show up for users.</p>
<p>I&#39;ll walk you through how we solved this by combining Next.js ISR revalidation with Netlify&#39;s cache tag purge API. We also leverage a Sitecore PowerShell script that triggers the endpoint for selected pages or items directly from the Content Editor. It&#39;s a pretty straightforward approach once you understand how Netlify&#39;s caching works.</p>
<hr>
<h2 id="the-problem">The Problem</h2>
<p>When we deployed our Next.js app with ISR to Netlify, we quickly noticed something wasn&#39;t right. Pages were staying cached at the edge even after we triggered revalidation. Here&#39;s what was happening:</p>
<ul>
<li><strong>Stale Content:</strong> Authors would publish updates in Sitecore and trigger revalidation, but users kept seeing the old version for way too long.</li>
<li><strong>ISR Limitations:</strong> The <code>res.revalidate(path)</code> method that works great on Vercel doesn&#39;t always make it through Netlify&#39;s edge network. It&#39;s hit or miss.</li>
<li><strong>No Granular Control:</strong> Without cache tags, we couldn&#39;t target specific pages. Our options were basically &quot;purge everything&quot; or &quot;wait for the cache to expire&quot; - neither is ideal.</li>
<li><strong>Author Experience:</strong> Content authors started losing trust in the system. They&#39;d constantly refresh to see if their changes were live, which led to support tickets and manual cache clearing.</li>
</ul>
<p>Netlify&#39;s edge caching is great for performance, but it works differently than what we were used to. We needed to work with Netlify&#39;s system, not against it.</p>
<hr>
<h2 id="the-solution">The Solution</h2>
<p>We ended up with a three-part solution that plays nicely with Netlify&#39;s caching:</p>
<ol>
<li><p><strong>Middleware Cache Tagging:</strong> We use Next.js middleware to stamp every HTML route with a <code>Netlify-Cache-Tag</code> header. This happens automatically before the page gets cached, so each page has a unique identifier.</p>
</li>
<li><p><strong>Revalidation API:</strong> Our <code>/api/revalidate</code> endpoint tries the standard Next.js ISR revalidation first (just in case it works), then calls Netlify&#39;s purge API with the matching cache tag. This way we cover both bases.</p>
</li>
<li><p><strong>Sitecore Integration:</strong> A simple PowerShell script in Sitecore calls our revalidation API when content gets published. Authors don&#39;t have to change their workflow at all.</p>
</li>
</ol>
<p>Here&#39;s the decision flow:</p>
<pre><code>Request for /en/products/abc
         └─&gt; Middleware intercepts
         |   └─&gt; Builds cache tag: page:en_products_abc
         |   └─&gt; Sets Netlify-Cache-Tag header
         |   └─&gt; Page cached with tag
         |
Content author publishes in Sitecore
         └─&gt; PowerShell script executes
         |   └─&gt; POST to /api/revalidate
         |       └─&gt; Verify secret
         |       └─&gt; Try res.revalidate(path) (best-effort)
         |       └─&gt; Build matching cache tag
         |       └─&gt; Call Netlify purge API with tag
         |       └─&gt; Netlify invalidates page across CDN
</code></pre>
<p>This gives us the best of both worlds: we keep the performance benefits of edge caching, but we can also invalidate specific pages whenever we need to. The cache tag system is the key - it lets us target exactly what we want to clear.</p>
<hr>
<h2 id="how-it-works">How It Works</h2>
<p>Let me break down the code so you can see how everything fits together.</p>
<h3 id="middlewarets---cache-tag-stamping"><code>middleware.ts</code> - Cache Tag Stamping</h3>
<p>This is where it all starts. The middleware runs before any page is rendered and stamps each route with a cache tag.</p>
<p><strong>File Name &amp; Role:</strong> <code>middleware.ts</code> (root of your Next.js app). This middleware intercepts requests and adds cache tag headers to HTML routes, so Netlify knows which tag belongs to which page.</p>
<p><strong>What it exports:</strong></p>
<ul>
<li><code>middleware</code>: The main function that processes requests</li>
<li><code>buildCacheTag</code>: A helper that converts paths into cache tag format</li>
</ul>
<p><strong>Code:</strong></p>
<pre><code class="language-ts">import type { NextRequest } from &#39;next/server&#39;;
import { NextResponse } from &#39;next/server&#39;;

function buildCacheTag(path: string): string {
  if (!path || path === &#39;/&#39;) {
    return &#39;page:home&#39;;
  }

  const normalized = path
    .trim()
    .replace(/^\/+|\/+$/g, &#39;&#39;)
    .replace(/\//g, &#39;_&#39;)
    .replace(/[^a-zA-Z0-9_-]/g, &#39;&#39;);

  return `page:${normalized || &#39;home&#39;}`;
}

export function middleware(req: NextRequest) {
  const pathname = req.nextUrl.pathname;

  if (
    pathname.startsWith(&#39;/api&#39;) ||
    pathname.startsWith(&#39;/_next&#39;) ||
    pathname.startsWith(&#39;/favicon&#39;) ||
    pathname.match(/\.(ico|png|jpg|jpeg|gif|webp|svg|css|js|txt|xml)$/)
  ) {
    return NextResponse.next();
  }

  const res = NextResponse.next();
  const cacheTag = buildCacheTag(pathname);

  res.headers.set(&#39;Netlify-Cache-Tag&#39;, cacheTag);
  res.headers.set(&#39;Cache-Tag&#39;, cacheTag);

  return res;
}

export const config = {
  matcher: [&#39;/((?!_next/static|_next/image|favicon.ico|api).*)&#39;],
};
</code></pre>
<p><strong>Why this works:</strong></p>
<ul>
<li><strong>Automatic Tagging:</strong> Every HTML route gets tagged automatically, so you don&#39;t have to think about it. It just works.</li>
<li><strong>Normalization:</strong> The <code>buildCacheTag</code> function turns paths like <code>/en/products/abc</code> into <code>page:en_products_abc</code>. This makes the tags predictable and URL-safe, and they match what we use in the revalidation API.</li>
<li><strong>Selective Application:</strong> We skip API routes, Next.js internals, and static files - they don&#39;t need cache tags anyway.</li>
<li><strong>Dual Headers:</strong> We set both <code>Netlify-Cache-Tag</code> and <code>Cache-Tag</code> headers. Netlify uses the first one, but the second is a standard that other CDNs recognize too, so you&#39;re covered if you switch platforms later.</li>
</ul>
<h3 id="pagesapirevalidatets---revalidation--purge"><code>pages/api/revalidate.ts</code> - Revalidation + Purge</h3>
<p>This is where the magic happens. The endpoint tries Next.js ISR revalidation first, then calls Netlify&#39;s purge API.</p>
<p><strong>File Name &amp; Role:</strong> <code>pages/api/revalidate.ts</code>. This API route handles revalidation requests from Sitecore. It tries ISR revalidation (just in case), then purges Netlify&#39;s cache using the cache tag.</p>
<p><strong>What it exports:</strong></p>
<ul>
<li><code>handler</code>: The main API route handler</li>
<li><code>RevalidateRequest</code>: TypeScript interface for the request body</li>
<li><code>buildCacheTag</code>: Same helper function as the middleware - the logic has to match exactly</li>
</ul>
<p><strong>Code:</strong></p>
<pre><code class="language-ts">import type { NextApiRequest, NextApiResponse } from &#39;next&#39;;

export interface RevalidateRequest {
  url?: string;
  secret?: string;
  siteName?: string;
}

function buildCacheTag(path: string): string {
  if (!path || path === &#39;/&#39;) {
    return &#39;page:home&#39;;
  }

  const normalized = path
    .trim()
    .replace(/^\/+|\/+$/g, &#39;&#39;)
    .replace(/\//g, &#39;_&#39;)
    .replace(/[^a-zA-Z0-9_-]/g, &#39;&#39;);

  return `page:${normalized || &#39;home&#39;}`;
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const revalidateRequest = req.body as RevalidateRequest;
  let revalidated = false;

  if (revalidateRequest.secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).end(&#39;Unauthorized&#39;);
  }

  let pathToClear = &#39;/&#39;;
  if (revalidateRequest?.url) {
    pathToClear = revalidateRequest.url;
  }

  if (!pathToClear) {
    return res.status(400).end(&#39;Revalidated : false&#39;);
  }

  try {
    await res.revalidate(pathToClear);
    revalidated = true;
  } catch (err) {
    // Continue with Netlify purge
  }

  try {
    const cacheTag = buildCacheTag(pathToClear);
    const siteId = process.env.NETLIFY_SITE_ID;
    const siteSlug = process.env.NETLIFY_SITE_SLUG;
    const token = process.env.NETLIFY_PAT;

    if (!token || (!siteId &amp;&amp; !siteSlug)) {
      return res.status(200).end(&#39;Revalidated : &#39; + revalidated);
    }

    const body: Record&lt;string, unknown&gt; = {
      cache_tags: [cacheTag],
    };

    if (siteId) {
      body.site_id = siteId;
    }

    if (siteSlug) {
      body.site_slug = siteSlug;
    }

    const purgeResp = await fetch(&#39;https://api.netlify.com/api/v1/purge&#39;, {
      method: &#39;POST&#39;,
      headers: {
        &#39;Content-Type&#39;: &#39;application/json&#39;,
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(body),
    });

    if (!purgeResp.ok) {
      const text = await purgeResp.text();
      throw new Error(`Netlify purge failed: ${purgeResp.status} ${text}`);
    }
  } catch (err) {
    // Continue even if purge fails
  }

  return res.status(200).end(&#39;Revalidated : &#39; + revalidated);
}
</code></pre>
<p><strong>Why this works:</strong></p>
<ul>
<li><strong>Dual Strategy:</strong> We try both ISR revalidation and Netlify purge. If one fails, the other might still work, so we&#39;re covered either way.</li>
<li><strong>Tag Matching:</strong> The <code>buildCacheTag</code> function uses the exact same logic as the middleware. This is crucial - the tag we purge has to match the tag that was set when the page was cached.</li>
<li><strong>Graceful Degradation:</strong> If Netlify config is missing, we still try ISR revalidation and return success. Better than breaking the authoring workflow.</li>
<li><strong>Security:</strong> The secret check keeps unauthorized people from clearing your cache.</li>
</ul>
<h3 id="sitecore-powershell-script">Sitecore PowerShell Script</h3>
<p>This PowerShell script hooks into Sitecore&#39;s context menu to trigger revalidation when content gets published.</p>
<p><strong>File Name &amp; Role:</strong> Sitecore PowerShell script (Context Menu). Authors right-click an item in Sitecore and select the revalidation option, which runs this script.</p>
<p><strong>Code:</strong></p>
<pre><code class="language-powershell">function ClearCacheForItem {
    Param ([Sitecore.Data.Items.Item]$item)
    
    $siteResolver = [Sitecore.DependencyInjection.ServiceLocator]::ServiceProvider.GetService([Sitecore.Sites.IItemSiteResolver])
    
    $defaultSiteInfo = $null
    foreach ($language in $item.Languages) {
        $langItem = Get-Item -Path &quot;master:&quot; -ID $item.ID -Language $language -ErrorAction SilentlyContinue
        if ($langItem -ne $null -and $langItem.Versions.Count -gt 0) {
            $testSiteInfo = $siteResolver.ResolveSite($langItem)
            if ($testSiteInfo -ne $null -and $testSiteInfo.TargetHostName -ne $null -and $testSiteInfo.TargetHostName -ne &quot;&quot;) {
                $defaultSiteInfo = $testSiteInfo
                break
            }
        }
    }
    
    if ($defaultSiteInfo -eq $null) {
        Write-Host &quot;ERROR: Could not find valid site information.&quot; -ForegroundColor Red
        return
    }
    
    foreach ($lang in $item.Languages) {
        $languageItem = Get-Item -Path &quot;master:&quot; -ID $item.ID -Language $lang
        if ($languageItem.Versions.Count -eq 0 -or $languageItem.Visualization.Layout -eq $null) {
            continue
        }
        
        $siteInfo = $siteResolver.ResolveSite($languageItem)
        if ($siteInfo -eq $null -or $siteInfo.TargetHostName -eq $null -or $siteInfo.TargetHostName -eq &quot;&quot;) {
            $siteInfo = $defaultSiteInfo
        }
        
        $scheme = if ($siteInfo.Scheme) { $siteInfo.Scheme } else { &quot;https&quot; }
        $cleanHostName = $siteInfo.TargetHostName
        if ($cleanHostName.StartsWith(&quot;www.&quot;)) {
            $cleanHostName = $cleanHostName.Substring(4)
        }
        $siteBaseUrl = &quot;$scheme://$cleanHostName&quot;
        
        $siteContext = New-Object Sitecore.Sites.SiteContext $siteInfo
        $options = New-Object Sitecore.Links.UrlOptions
        $options.AlwaysIncludeServerUrl = $false
        $options.Language = $lang
        $options.SiteResolving = $true
        $options.Site = $siteContext
        $options.LanguageEmbedding = [Sitecore.Links.LanguageEmbedding]::Always
        
        $itemUrl = [Sitecore.Links.LinkManager]::GetItemUrl($languageItem, $options)
        $itemRelativeUrl = $itemUrl
        
        if ($itemUrl.Contains($siteInfo.TargetHostName)) {
            $itemRelativeUrl = $itemUrl.Substring($itemUrl.IndexOf($siteInfo.TargetHostName) + $siteInfo.TargetHostName.Length)
        }
        
        $revalidatePostUrl = &quot;$siteBaseUrl/api/revalidate&quot;
        $body = @{
            url = $itemRelativeUrl
            secret = &quot;YOUR_REVALIDATE_SECRET&quot;
            siteName = $siteInfo.Name
        } | ConvertTo-Json
        
        try {
            $response = Invoke-RestMethod -Uri $revalidatePostUrl -Method Post -Body $body -ContentType &quot;application/json&quot;
            Write-Host &quot;Response: $response&quot; -ForegroundColor Green
        } catch {
            Write-Host &quot;ERROR: Failed to invalidate URL: $($_.Exception.Message)&quot; -ForegroundColor Red
        }
    }
}

$item = Get-Item -Path .
ClearCacheForItem -item $item
</code></pre>
<p><strong>Why this works:</strong></p>
<ul>
<li>The script automatically triggers cache invalidation when content is published.</li>
<li>It handles multiple sites and languages without any extra work.</li>
<li>If something goes wrong, it logs the error but doesn&#39;t break the authoring experience.</li>
</ul>
<hr>
<h2 id="getting-started">Getting Started</h2>
<p>Here&#39;s what you need to do to get this set up:</p>
<ol>
<li><p><strong>Add Middleware:</strong> Create <code>middleware.ts</code> in the root of your Next.js app. This automatically stamps cache tags on all HTML routes.</p>
</li>
<li><p><strong>Create Revalidation API:</strong> Add <code>pages/api/revalidate.ts</code> with the revalidation logic that calls Netlify&#39;s purge API.</p>
</li>
<li><p><strong>Configure Environment Variables:</strong> In Netlify, add these environment variables:</p>
<ul>
<li><code>REVALIDATE_SECRET</code>: A secure secret for API authentication</li>
<li><code>NETLIFY_PAT</code>: Your Netlify Personal Access Token</li>
<li><code>NETLIFY_SITE_ID</code> or <code>NETLIFY_SITE_SLUG</code>: Your Netlify site identifier</li>
</ul>
</li>
<li><p><strong>Update PowerShell Script:</strong> Replace your existing Sitecore PowerShell script with the version above, and update the <code>REVALIDATE_SECRET</code> value.</p>
</li>
<li><p><strong>Test It Out:</strong> </p>
<ul>
<li>Publish a page in Sitecore</li>
<li>Trigger revalidation via the context menu</li>
<li>Check that the page gets purged from Netlify&#39;s cache within a few seconds</li>
<li>Verify in Netlify&#39;s dashboard that cache tag purges are happening</li>
</ul>
</li>
<li><p><strong>Keep an Eye on It:</strong> Watch for any edge cases with path normalization or cache tag matching. You might need to tweak the <code>buildCacheTag</code> function based on your URL structure.</p>
</li>
</ol>
<p>That&#39;s it! You&#39;ll have a cache invalidation system that works with Netlify&#39;s edge network while keeping all the performance benefits of CDN caching.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Integrating SAP Customer Data Cloud with Sitecore XM Cloud Next.js - A Complete Implementation Guide</title>
      <link>https://alikhan.tech/blogs/2025/integrating-sap-cdc-sitecore-xm-cloud-nextjs</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/integrating-sap-cdc-sitecore-xm-cloud-nextjs</guid>
      <pubDate>Wed, 29 Oct 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[I recently got the chance to implement SAP Customer Data Cloud (formerly Gigya) authentication in our Sitecore XM Cloud Next.js application. The goal was to create an authentication system that integrates with Sitecore's headless architecture while providing secure JWT-based…]]></description>
      <content:encoded><![CDATA[<p>I recently got the chance to implement SAP Customer Data Cloud (formerly Gigya) authentication in our Sitecore XM Cloud Next.js application. The goal was to create an authentication system that integrates with Sitecore&#39;s headless architecture while providing secure JWT-based authentication, session management, and SSO capabilities.</p>
<p>While SAP CDC is a powerful CIAM platform, integrating it with Next.js and Sitecore presented several unique challenges—from JWT verification complexities to session persistence issues and middleware configuration. This post documents the complete implementation, the challenges we encountered, and how we solved them.</p>
<hr>
<h2 id="architecture-overview">Architecture Overview</h2>
<p>The authentication flow combines SAP CDC&#39;s CIAM platform with NextAuth.js to provide JWT-based authentication for our Sitecore application. Here&#39;s how it works:</p>
<ol>
<li><strong>User Authentication</strong>: Users authenticate through SAP CDC&#39;s screen-sets (customizable login UI)</li>
<li><strong>JWT Generation</strong>: SAP CDC generates a JWT token after successful authentication</li>
<li><strong>Token Verification</strong>: NextAuth verifies the JWT using SAP CDC&#39;s public key</li>
<li><strong>Session Management</strong>: NextAuth creates and manages server-side sessions</li>
<li><strong>Route Protection</strong>: Next.js middleware protects Sitecore routes based on session state</li>
</ol>
<p>This approach allows us to leverage SAP CDC&#39;s authentication capabilities while maintaining compatibility with Sitecore&#39;s headless architecture and Next.js routing.</p>
<hr>
<h2 id="the-challenges-we-faced">The Challenges We Faced</h2>
<h3 id="challenge-1-jwt-verification-with-dynamic-public-keys">Challenge 1: JWT Verification with Dynamic Public Keys</h3>
<p><strong>Problem</strong>: SAP CDC uses RSA256 JWT tokens, but the public key must be fetched dynamically from their API. Initially, we tried to cache the public key, but this caused issues when SAP CDC rotated keys.</p>
<p><strong>Solution</strong>: We implemented a JWT verification system that fetches the public key on-demand and handles key rotation gracefully:</p>
<pre><code class="language-typescript">async verifyIdToken(idToken: string) {
  try {
    // Fetch SAP CDC public key dynamically
    const gigyaUsApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_US_API_BASE_URL || &#39;https://accounts.us1.gigya.com&#39;;
    const pubKeyResp = await fetch(
      `${gigyaUsApiBaseUrl}/accounts.getJWTPublicKey?apiKey=${process.env.NEXT_PUBLIC_GIGYA_API_KEY}&amp;format=json`
    );
    const pubKeyData = await pubKeyResp.json();

    if (pubKeyData.errorCode &amp;&amp; pubKeyData.errorCode !== 0) {
      throw new Error(&#39;Failed to fetch SAP CDC public key&#39;);
    }

    const { n, e } = pubKeyData;
    if (!n || !e) throw new Error(&#39;Public key data is incomplete&#39;);

    // Verify JWT using jose library
    const jwk = { kty: &#39;RSA&#39;, n, e };
    const key = await jose.importJWK(jwk, &#39;RS256&#39;);
    const { payload } = await jose.jwtVerify(idToken, key);

    return payload;
  } catch (error) {
    console.error(&#39;SAP CDC JWT verification failed:&#39;, error);
    throw error;
  }
}
</code></pre>
<h3 id="challenge-2-session-persistence-across-page-reloads">Challenge 2: Session Persistence Across Page Reloads</h3>
<p><strong>Problem</strong>: Users were being logged out on page refresh, even though their SAP CDC session was still valid. The issue was that NextAuth wasn&#39;t properly syncing with the SAP CDC session state.</p>
<p><strong>Solution</strong>: We implemented a session verification mechanism that checks SAP CDC session status before showing the login screen:</p>
<pre><code class="language-typescript">function verifyGigyaSession(): Promise&lt;boolean&gt; {
  return new Promise((resolve) =&gt; {
    const w = window as any;
    if (!w.gigya?.accounts?.getAccountInfo) return resolve(false);
    
    w.gigya.accounts.getAccountInfo({
      callback: (res: any) =&gt; resolve(res.errorCode === 0),
    });
  });
}
</code></pre>
<p>Then in the login page, we check for existing sessions before showing the login form:</p>
<pre><code class="language-typescript">useEffect(() =&gt; {
  loadGigyaScript()
    .then(async () =&gt; {
      const hasSession = await verifyGigyaSession();

      if (hasSession) {
        // User already has SAP CDC session, establish NextAuth session
        const w = window as any;
        w.gigya?.accounts?.getJWT({
          fields: &#39;profile,email,emails,data&#39;,
          expiration: 600,
          callback: async (jwtRes: any) =&gt; {
            if (jwtRes.errorCode === 0 &amp;&amp; jwtRes.id_token) {
              const result = await establishAppSession(jwtRes.id_token);
              if (result.ok) {
                window.location.href = callbackUrl || &#39;/my-account&#39;;
              }
            }
          },
        });
        return;
      }

      showScreenSet();
    });
}, []);
</code></pre>
<h3 id="challenge-3-middleware-route-protection">Challenge 3: Middleware Route Protection</h3>
<p><strong>Problem</strong>: Protecting Sitecore routes with middleware while maintaining compatibility with Sitecore&#39;s JSS middleware was tricky. We needed to ensure authentication checks happened at the right time without breaking Sitecore&#39;s routing.</p>
<p><strong>Solution</strong>: We created a middleware that runs after Sitecore&#39;s JSS middleware and checks for authentication tokens:</p>
<pre><code class="language-typescript">export async function middleware(req: NextRequest) {
  const pathname = req.nextUrl.pathname;

  // Define protected Sitecore routes
  const protectedRoutes = [&#39;/my-account&#39;, &#39;/profile&#39;, &#39;/settings&#39;];

  const isProtectedRoute = protectedRoutes.some(route =&gt; pathname.startsWith(route));

  if (isProtectedRoute) {
    const token = await getToken({
      req,
      secret: process.env.NEXTAUTH_SECRET,
    });

    if (!token) {
      const url = req.nextUrl.clone();
      url.pathname = &#39;/login&#39;;
      url.searchParams.set(&#39;callbackUrl&#39;, pathname);
      return NextResponse.redirect(url);
    }
  }

  return NextResponse.next();
}
</code></pre>
<h3 id="challenge-4-sdk-loading-race-conditions">Challenge 4: SDK Loading Race Conditions</h3>
<p><strong>Problem</strong>: The SAP CDC SDK is loaded asynchronously, and we were trying to use it before it was ready, causing &quot;gigya is not defined&quot; errors.</p>
<p><strong>Solution</strong>: We implemented a proper SDK loading mechanism with Promise-based initialization:</p>
<pre><code class="language-typescript">function loadGigyaScript() {
  return new Promise&lt;void&gt;((resolve, reject) =&gt; {
    if ((window as any).gigya) return resolve();
    
    const script = document.createElement(&#39;script&#39;);
    const gigyaCdnBaseUrl = process.env.NEXT_PUBLIC_GIGYA_CDN_BASE_URL || &#39;https://cdns.us1.gigya.com&#39;;
    script.src = `${gigyaCdnBaseUrl}/js/gigya.js?apiKey=${GIGYA_API_KEY}`;
    script.async = true;
    script.onload = () =&gt; resolve();
    script.onerror = () =&gt; reject(new Error(&#39;Failed to load SAP CDC SDK&#39;));
    document.body.appendChild(script);
  });
}
</code></pre>
<h3 id="challenge-5-logout-synchronization">Challenge 5: Logout Synchronization</h3>
<p><strong>Problem</strong>: Logging out from NextAuth didn&#39;t clear the SAP CDC session, and vice versa, leading to inconsistent authentication state.</p>
<p><strong>Solution</strong>: We created a unified logout function that clears both sessions:</p>
<pre><code class="language-typescript">export async function logoutFromGigya(): Promise&lt;void&gt; {
  return new Promise&lt;void&gt;((resolve) =&gt; {
    try {
      const gigyaWindow = window as any;
      if (typeof window !== &#39;undefined&#39; &amp;&amp; gigyaWindow?.gigya?.accounts?.logout) {
        gigyaWindow.gigya.accounts.logout({
          callback: () =&gt; {
            console.log(&#39;SAP CDC logout successful&#39;);
            resolve();
          },
        });
      } else {
        resolve();
      }
    } catch (error) {
      console.error(&#39;Error during SAP CDC logout:&#39;, error);
      resolve();
    }
  });
}

// In logout component
const handleSignOut = async () =&gt; {
  await logoutFromGigya();  // Clear SAP CDC session first
  await signOut({ callbackUrl: &#39;/login&#39;, redirect: false });
  window.location.href = &#39;/login&#39;;
};
</code></pre>
<hr>
<h2 id="prerequisites">Prerequisites</h2>
<p><strong>What you need:</strong></p>
<ul>
<li>Sitecore XM Cloud instance with JSS Next.js app</li>
<li>SAP CDC account (API Key, User Key, Secret Key)</li>
<li>Node.js 18+, Next.js 14+</li>
</ul>
<p><strong>Get your SAP CDC credentials:</strong></p>
<ol>
<li>Log into SAP CDC Console → Admin → Site Settings → API Keys</li>
<li>Note your API Key, User Key, and Secret Key</li>
</ol>
<hr>
<h2 id="implementation-guide">Implementation Guide</h2>
<h3 id="step-1-install-dependencies">Step 1: Install Dependencies</h3>
<pre><code class="language-bash">npm install next-auth jose
</code></pre>
<h3 id="step-2-configure-environment-variables">Step 2: Configure Environment Variables</h3>
<p>Create <code>.env.local</code> in your Sitecore Next.js app root:</p>
<pre><code class="language-env"># NextAuth (generate secret: openssl rand -base64 32)
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_secret_here
NEXTAUTH_JWT_SECRET=your_jwt_secret_here

# SAP CDC Public (from SAP CDC Console)
NEXT_PUBLIC_GIGYA_API_KEY=your_api_key
NEXT_PUBLIC_GIGYA_SCREENSET=Default-RegistrationLogin
NEXT_PUBLIC_GIGYA_START_SCREEN=gigya-login-screen

# SAP CDC Data Center (adjust us1/eu1/au1 based on region)
NEXT_PUBLIC_GIGYA_CDN_BASE_URL=https://cdns.us1.gigya.com
NEXT_PUBLIC_GIGYA_API_BASE_URL=https://accounts.gigya.com
NEXT_PUBLIC_GIGYA_US_API_BASE_URL=

# SAP CDC Private (server-side only - KEEP SECRET)
GIGYA_USER_KEY=your_user_key
GIGYA_SECRET=your_secret

# Optional
NEXT_PUBLIC_SESSION_TIMEOUT_MINUTES=30
</code></pre>
<p>⚠️ <strong>Important</strong>: Add <code>.env.local</code> to <code>.gitignore</code>!</p>
<hr>
<h3 id="step-3-extend-nextauth-types">Step 3: Extend NextAuth Types</h3>
<p><strong>Create</strong> <code>src/types/next-auth.d.ts</code>:</p>
<pre><code class="language-typescript">import { DefaultSession } from &#39;next-auth&#39;;

declare module &#39;next-auth&#39; {
  interface Session extends DefaultSession {
    gigyaProfile?: Record&lt;string, unknown&gt;;
    cardNumber?: string;  // Replace with your Sitecore user identifier
    gigyaToken?: string;
    isFirstLogin?: boolean;
  }

  interface User {
    gigyaProfile?: Record&lt;string, unknown&gt;;
    cardNumber?: string;
    gigyaToken?: string;
    isFirstLogin?: boolean;
  }
}

declare module &#39;next-auth/jwt&#39; {
  interface JWT {
    gigyaProfile?: Record&lt;string, unknown&gt;;
    cardNumber?: string;
    gigyaToken?: string;
    isFirstLogin?: boolean;
  }
}
</code></pre>
<hr>
<h3 id="step-4-create-sap-cdc-service">Step 4: Create SAP CDC Service</h3>
<p><strong>Create</strong> <code>src/lib/gigya/gigya-service.ts</code>:</p>
<pre><code class="language-typescript">/* eslint-disable @typescript-eslint/no-explicit-any */

interface GigyaConfig {
  apiKey: string;
  userKey: string;
  secret: string;
}

interface GigyaAccountInfo {
  UID: string;
  profile?: { firstName?: string; lastName?: string; email?: string };
  data?: { CardNumber?: string; FirstLogin?: boolean };  // Adjust for your Sitecore schema
  loginIDs?: { emails?: string[] };
  errorCode?: number;
  errorMessage?: string;
}

class GigyaService {
  private config: GigyaConfig;

  constructor(config: GigyaConfig) {
    this.config = config;
  }

  async getAccountInfo(uid: string): Promise&lt;GigyaAccountInfo | null&gt; {
    try {
      const formData = new URLSearchParams();
      formData.append(&#39;apiKey&#39;, this.config.apiKey);
      formData.append(&#39;userKey&#39;, this.config.userKey);
      formData.append(&#39;secret&#39;, this.config.secret);
      formData.append(&#39;UID&#39;, uid);
      formData.append(&#39;include&#39;, &#39;emails,data,profile,loginIDs&#39;);

      const gigyaApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_API_BASE_URL || &#39;https://accounts.gigya.com&#39;;
      const response = await fetch(`${gigyaApiBaseUrl}/accounts.getAccountInfo`, {
        method: &#39;POST&#39;,
        headers: { &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39; },
        body: formData,
      });

      const result = await response.json();
      if (result.errorCode !== 0) throw new Error(`SAP CDC error: ${result.errorMessage}`);
      return result;
    } catch (error) {
      console.error(&#39;Error getting SAP CDC account info:&#39;, error);
      return null;
    }
  }

  async getJWT(uid: string, fields: string = &#39;data.CardNumber&#39;): Promise&lt;string | null&gt; {
    try {
      const formData = new URLSearchParams();
      formData.append(&#39;apiKey&#39;, this.config.apiKey);
      formData.append(&#39;userKey&#39;, this.config.userKey);
      formData.append(&#39;secret&#39;, this.config.secret);
      formData.append(&#39;targetUID&#39;, uid);
      formData.append(&#39;fields&#39;, fields);

      const gigyaApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_API_BASE_URL || &#39;https://accounts.gigya.com&#39;;
      const response = await fetch(`${gigyaApiBaseUrl}/accounts.getJWT`, {
        method: &#39;POST&#39;,
        headers: { &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39; },
        body: formData,
      });

      const result = await response.json();
      if (result.errorCode !== 0) throw new Error(`SAP CDC error: ${result.errorMessage}`);
      return result.id_token;
    } catch (error) {
      console.error(&#39;Error getting SAP CDC JWT:&#39;, error);
      return null;
    }
  }

  async setAccountInfo(uid: string, data: any): Promise&lt;boolean&gt; {
    try {
      const formData = new URLSearchParams();
      formData.append(&#39;apiKey&#39;, this.config.apiKey);
      formData.append(&#39;userKey&#39;, this.config.userKey);
      formData.append(&#39;secret&#39;, this.config.secret);
      formData.append(&#39;UID&#39;, uid);
      formData.append(&#39;data&#39;, JSON.stringify(data));

      const gigyaApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_API_BASE_URL || &#39;https://accounts.gigya.com&#39;;
      const response = await fetch(`${gigyaApiBaseUrl}/accounts.setAccountInfo`, {
        method: &#39;POST&#39;,
        headers: { &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39; },
        body: formData,
      });

      const result = await response.json();
      if (result.errorCode !== 0) throw new Error(`SAP CDC error: ${result.errorMessage}`);
      return true;
    } catch (error) {
      console.error(&#39;Error setting SAP CDC account info:&#39;, error);
      return false;
    }
  }
}

// Singleton instance
let gigyaServiceInstance: GigyaService | null = null;

export function getGigyaService(): GigyaService {
  if (!gigyaServiceInstance) {
    gigyaServiceInstance = new GigyaService({
      apiKey: process.env.NEXT_PUBLIC_GIGYA_API_KEY || &#39;&#39;,
      userKey: process.env.GIGYA_USER_KEY || &#39;&#39;,
      secret: process.env.GIGYA_SECRET || &#39;&#39;,
    });
  }
  return gigyaServiceInstance;
}

// Client-side logout helper
export async function logoutFromGigya(): Promise&lt;void&gt; {
  return new Promise&lt;void&gt;((resolve) =&gt; {
    try {
      const gigyaWindow = window as any;
      if (typeof window !== &#39;undefined&#39; &amp;&amp; gigyaWindow?.gigya?.accounts?.logout) {
        gigyaWindow.gigya.accounts.logout({
          callback: () =&gt; {
            console.log(&#39;SAP CDC logout successful&#39;);
            resolve();
          },
        });
      } else {
        resolve();
      }
    } catch (error) {
      console.error(&#39;Error during SAP CDC logout:&#39;, error);
      resolve();
    }
  });
}

export default GigyaService;
</code></pre>
<hr>
<h3 id="step-5-create-custom-nextauth-provider">Step 5: Create Custom NextAuth Provider</h3>
<p><strong>Create</strong> <code>src/pages/api/auth/[...nextauth].ts</code>:</p>
<pre><code class="language-typescript">/* eslint-disable @typescript-eslint/no-explicit-any */
import { getGigyaService } from &#39;@/lib/gigya/gigya-service&#39;;
import * as jose from &#39;jose&#39;;
import { NextApiRequest, NextApiResponse } from &#39;next&#39;;
import NextAuth, { NextAuthOptions } from &#39;next-auth&#39;;

// Custom SAP CDC Provider for Sitecore
const GigyaProvider = {
  id: &#39;gigya&#39;,
  name: &#39;SAP Customer Data Cloud&#39;,
  type: &#39;credentials&#39; as const,
  credentials: {
    id_token: { label: &#39;ID Token&#39;, type: &#39;text&#39; },
  },

  async verifyIdToken(idToken: string) {
    try {
      // Fetch SAP CDC public key
      const gigyaUsApiBaseUrl = process.env.NEXT_PUBLIC_GIGYA_US_API_BASE_URL || &#39;https://accounts.us1.gigya.com&#39;;
      const pubKeyResp = await fetch(
        `${gigyaUsApiBaseUrl}/accounts.getJWTPublicKey?apiKey=${process.env.NEXT_PUBLIC_GIGYA_API_KEY}&amp;format=json`
      );
      const pubKeyData = await pubKeyResp.json();

      if (pubKeyData.errorCode &amp;&amp; pubKeyData.errorCode !== 0) {
        throw new Error(&#39;Failed to fetch SAP CDC public key&#39;);
      }

      const { n, e } = pubKeyData;
      if (!n || !e) throw new Error(&#39;Public key data is incomplete&#39;);

      // Verify JWT
      const jwk = { kty: &#39;RSA&#39;, n, e };
      const key = await jose.importJWK(jwk, &#39;RS256&#39;);
      const { payload } = await jose.jwtVerify(idToken, key);

      return payload;
    } catch (error) {
      console.error(&#39;SAP CDC JWT verification failed:&#39;, error);
      throw error;
    }
  },

  async authorize(credentials: any) {
    try {
      if (!credentials?.id_token) {
        throw new Error(&#39;No ID token provided&#39;);
      }

      const payload = await this.verifyIdToken(credentials.id_token);
      const uid = String(payload.sub || payload.uid || &#39;&#39;);

      if (!uid) throw new Error(&#39;Invalid user ID in token&#39;);

      // Get user info from SAP CDC
      const gigyaService = getGigyaService();
      const accountInfo = await gigyaService.getAccountInfo(uid);

      if (!accountInfo) throw new Error(&#39;Failed to get SAP CDC account info&#39;);

      // Get or generate user identifier (adjust for your Sitecore setup)
      let cardNumber = accountInfo.data?.CardNumber || &#39;&#39;;
      if (!cardNumber) {
        cardNumber = `TEMP_${uid.substring(0, 8).toUpperCase()}`;
        await gigyaService.setAccountInfo(uid, { CardNumber: cardNumber });
      }

      return {
        id: uid,
        email: accountInfo.profile?.email || &#39;&#39;,
        name: accountInfo.profile?.firstName || &#39;&#39;,
        gigyaProfile: payload,
        cardNumber: cardNumber,
      };
    } catch (error) {
      console.error(&#39;SAP CDC authorization failed:&#39;, error);
      return null;
    }
  },
};

// NextAuth configuration for Sitecore
export const authOptions: NextAuthOptions = {
  providers: [GigyaProvider as any],
  session: {
    strategy: &#39;jwt&#39;,
    maxAge: 30 * 60, // 30 minutes
  },
  pages: {
    signIn: &#39;/login&#39;,
  },
  callbacks: {
    async session({ session, token }) {
      if (token) {
        session.gigyaProfile = token.gigyaProfile as Record&lt;string, unknown&gt;;
        session.cardNumber = token.cardNumber as string;
        session.gigyaToken = token.gigyaToken as string;
      }
      return session;
    },
    async jwt({ token, user }) {
      const isDuringSignin = !!user;
      
      if (isDuringSignin &amp;&amp; user &amp;&amp; &#39;gigyaProfile&#39; in user) {
        token.gigyaProfile = (user as any).gigyaProfile;
        token.cardNumber = (user as any).cardNumber;

        // Generate SAP CDC JWT for API calls
        const gigyaService = getGigyaService();
        const uid = String(token.gigyaProfile?.uid || user.id || &#39;&#39;);
        
        if (uid) {
          try {
            const gigyaToken = await gigyaService.getJWT(uid);
            if (gigyaToken) {
              token.gigyaToken = gigyaToken;
            }
          } catch (error) {
            console.error(&#39;Error getting SAP CDC JWT:&#39;, error);
          }
        }
      }

      return token;
    },
  },
  events: {
    async signOut({ token }) {
      console.log(&#39;User signed out:&#39;, {
        hasGigyaProfile: !!token?.gigyaProfile,
        cardNumber: token?.cardNumber,
      });
    },
  },
  debug: false,
};

export default async function auth(req: NextApiRequest, res: NextApiResponse) {
  return await NextAuth(req, res, authOptions);
}
</code></pre>
<p><strong>Key Points:</strong></p>
<ul>
<li>JWT verification uses SAP CDC&#39;s public key (RSA256)</li>
<li>Session stores minimal data in encrypted HttpOnly cookies</li>
<li>Works with Sitecore&#39;s headless architecture</li>
</ul>
<hr>
<h3 id="step-6-create-login-page">Step 6: Create Login Page</h3>
<p><strong>Create</strong> <code>src/pages/login.tsx</code>:</p>
<pre><code class="language-typescript">/* eslint-disable @typescript-eslint/no-explicit-any */
import { signIn } from &#39;next-auth/react&#39;;
import { useCallback, useEffect, useState } from &#39;react&#39;;

declare global {
  interface Window {
    gigya: any;
  }
}

const GIGYA_API_KEY = process.env.NEXT_PUBLIC_GIGYA_API_KEY || &#39;&#39;;
const GIGYA_SCREENSET = process.env.NEXT_PUBLIC_GIGYA_SCREENSET || &#39;Default-RegistrationLogin&#39;;
const GIGYA_START_SCREEN = process.env.NEXT_PUBLIC_GIGYA_START_SCREEN || &#39;gigya-login-screen&#39;;

function loadGigyaScript() {
  return new Promise&lt;void&gt;((resolve, reject) =&gt; {
    if ((window as any).gigya) return resolve();
    
    const script = document.createElement(&#39;script&#39;);
    const gigyaCdnBaseUrl = process.env.NEXT_PUBLIC_GIGYA_CDN_BASE_URL || &#39;https://cdns.us1.gigya.com&#39;;
    script.src = `${gigyaCdnBaseUrl}/js/gigya.js?apiKey=${GIGYA_API_KEY}`;
    script.async = true;
    script.onload = () =&gt; resolve();
    script.onerror = () =&gt; reject(new Error(&#39;Failed to load SAP CDC SDK&#39;));
    document.body.appendChild(script);
  });
}

function verifyGigyaSession(): Promise&lt;boolean&gt; {
  return new Promise((resolve) =&gt; {
    const w = window as any;
    if (!w.gigya?.accounts?.getAccountInfo) return resolve(false);
    
    w.gigya.accounts.getAccountInfo({
      callback: (res: any) =&gt; resolve(res.errorCode === 0),
    });
  });
}

export default function LoginPage() {
  const [sdkReady, setSdkReady] = useState(false);
  const [error, setError] = useState(&#39;&#39;);

  const callbackUrl = 
    typeof window !== &#39;undefined&#39;
      ? new URLSearchParams(window.location.search).get(&#39;callbackUrl&#39;)
      : null;

  const establishAppSession = useCallback(async (idToken: string) =&gt; {
    try {
      const result = await signIn(&#39;gigya&#39;, {
        id_token: idToken,
        redirect: false,
      });

      return result?.ok ? { ok: true } : { ok: false, error: result?.error };
    } catch (err: any) {
      return { ok: false, error: err.message };
    }
  }, []);

  const handleOnLogin = useCallback(() =&gt; {
    const w = window as any;

    w.gigya.accounts.getJWT({
      fields: &#39;profile,email,emails,data&#39;,
      expiration: 600,
      callback: async (jwtRes: any) =&gt; {
        if (jwtRes.errorCode === 0 &amp;&amp; jwtRes.id_token) {
          const result = await establishAppSession(jwtRes.id_token);
          
          if (result.ok) {
            window.location.href = callbackUrl || &#39;/my-account&#39;;  // Adjust redirect URL
          } else {
            setError(&#39;Session establishment failed: &#39; + (result.error || &#39;Unknown error&#39;));
          }
        } else {
          setError(&#39;JWT generation failed: &#39; + (jwtRes?.errorMessage || jwtRes?.errorCode));
        }
      },
    });
  }, [establishAppSession, callbackUrl]);

  const showScreenSet = useCallback(() =&gt; {
    const w = window as any;
    if (!w.gigya?.accounts?.showScreenSet) return;

    w.gigya.accounts.addEventHandlers({
      onLogin: handleOnLogin,
    });

    w.gigya.accounts.showScreenSet({
      screenSet: GIGYA_SCREENSET,
      startScreen: GIGYA_START_SCREEN,
      onError: (e: any) =&gt; setError(&#39;Screen-Set error: &#39; + (e?.errorMessage || e?.errorCode)),
    });
  }, [handleOnLogin]);

  useEffect(() =&gt; {
    let mounted = true;

    loadGigyaScript()
      .then(async () =&gt; {
        if (!mounted) return;
        setSdkReady(true);

        const hasSession = await verifyGigyaSession();

        if (hasSession) {
          const w = window as any;
          w.gigya?.accounts?.getJWT({
            fields: &#39;profile,email,emails,data&#39;,
            expiration: 600,
            callback: async (jwtRes: any) =&gt; {
              if (jwtRes.errorCode === 0 &amp;&amp; jwtRes.id_token) {
                const result = await establishAppSession(jwtRes.id_token);
                if (result.ok) {
                  window.location.href = callbackUrl || &#39;/my-account&#39;;
                }
              }
            },
          });
          return;
        }

        showScreenSet();
      })
      .catch((e) =&gt; mounted &amp;&amp; setError(String(e?.message || e)));

    return () =&gt; {
      mounted = false;
    };
  }, [establishAppSession, callbackUrl, showScreenSet]);

  return (
    &lt;div&gt;
      &lt;h1&gt;Sign In&lt;/h1&gt;
      
      {!sdkReady &amp;&amp; &lt;div&gt;Loading...&lt;/div&gt;}
      
      {error &amp;&amp; &lt;div&gt;{error}&lt;/div&gt;}
    &lt;/div&gt;
  );
}

export async function getServerSideProps() {
  return { props: {} };
}
</code></pre>
<p><strong>Sitecore Note</strong>: The screen-set displays your branded UI - customize it in SAP CDC Console to match your Sitecore site design.</p>
<hr>
<h3 id="step-7-load-sap-cdc-sdk-globally">Step 7: Load SAP CDC SDK Globally</h3>
<p><strong>Update</strong> your <code>src/Layout.tsx</code> or <code>src/pages/_app.tsx</code>:</p>
<pre><code class="language-typescript">import Script from &#39;next/script&#39;;

// In your Layout component
&lt;Script
  src={`${
    process.env.NEXT_PUBLIC_GIGYA_CDN_BASE_URL || &#39;&#39;
  }/js/gigya.js?apiKey=${process.env.NEXT_PUBLIC_GIGYA_API_KEY}`}
  strategy=&quot;afterInteractive&quot;
/&gt;
</code></pre>
<p>This loads the SAP CDC SDK globally for SSO and persistent sessions across your Sitecore site.</p>
<hr>
<h3 id="step-8-add-session-timeout">Step 8: Add Session Timeout</h3>
<p><strong>Create</strong> <code>src/components/SessionTimeoutChecker.tsx</code>:</p>
<pre><code class="language-typescript">import { useSession, signOut } from &#39;next-auth/react&#39;;
import { useEffect } from &#39;react&#39;;
import { logoutFromGigya } from &#39;@/lib/gigya/gigya-service&#39;;

export function SessionTimeoutChecker() {
  const { data: session, status } = useSession();

  useEffect(() =&gt; {
    if (status !== &#39;authenticated&#39; || !session) return;

    const timeoutMinutes = Number(process.env.NEXT_PUBLIC_SESSION_TIMEOUT_MINUTES) || 30;
    const timeoutMs = timeoutMinutes * 60 * 1000;

    let lastActivity = Date.now();
    const resetActivity = () =&gt; (lastActivity = Date.now());

    const events = [&#39;mousedown&#39;, &#39;keydown&#39;, &#39;scroll&#39;, &#39;touchstart&#39;];
    events.forEach((event) =&gt; window.addEventListener(event, resetActivity));

    const interval = setInterval(async () =&gt; {
      if (Date.now() - lastActivity &gt;= timeoutMs) {
        await logoutFromGigya();
        await signOut({ callbackUrl: &#39;/login?reason=timeout&#39;, redirect: false });
        window.location.href = &#39;/login?reason=timeout&#39;;
      }
    }, 60 * 1000);

    return () =&gt; {
      events.forEach((event) =&gt; window.removeEventListener(event, resetActivity));
      clearInterval(interval);
    };
  }, [session, status]);

  return null;
}
</code></pre>
<p>Add to your <code>_app.tsx</code>:</p>
<pre><code class="language-typescript">import { SessionProvider } from &#39;next-auth/react&#39;;
import { SessionTimeoutChecker } from &#39;@/components/SessionTimeoutChecker&#39;;

function MyApp({ Component, pageProps: { session, ...pageProps } }) {
  return (
    &lt;SessionProvider session={session}&gt;
      &lt;SessionTimeoutChecker /&gt;
      &lt;Component {...pageProps} /&gt;
    &lt;/SessionProvider&gt;
  );
}
</code></pre>
<hr>
<h3 id="step-9-add-logout">Step 9: Add Logout</h3>
<p><strong>In your Sitecore header component:</strong></p>
<pre><code class="language-typescript">import { signOut } from &#39;next-auth/react&#39;;
import { logoutFromGigya } from &#39;@/lib/gigya/gigya-service&#39;;

export function LogoutButton() {
  const handleSignOut = async () =&gt; {
    await logoutFromGigya();  // Clear SAP CDC session first
    await signOut({ callbackUrl: &#39;/login&#39;, redirect: false });
    window.location.href = &#39;/login&#39;;
  };

  return &lt;button onClick={handleSignOut}&gt;Sign Out&lt;/button&gt;;
}
</code></pre>
<hr>
<h3 id="step-10-protect-sitecore-routes">Step 10: Protect Sitecore Routes</h3>
<p><strong>Create</strong> <code>src/middleware.ts</code>:</p>
<pre><code class="language-typescript">import { getToken } from &#39;next-auth/jwt&#39;;
import { NextRequest, NextResponse } from &#39;next/server&#39;;

export async function middleware(req: NextRequest) {
  const pathname = req.nextUrl.pathname;

  // Define protected Sitecore routes
  const protectedRoutes = [&#39;/my-account&#39;, &#39;/profile&#39;, &#39;/settings&#39;];

  const isProtectedRoute = protectedRoutes.some(route =&gt; pathname.startsWith(route));

  if (isProtectedRoute) {
    const token = await getToken({
      req,
      secret: process.env.NEXTAUTH_SECRET,
    });

    if (!token) {
      const url = req.nextUrl.clone();
      url.pathname = &#39;/login&#39;;
      url.searchParams.set(&#39;callbackUrl&#39;, pathname);
      return NextResponse.redirect(url);
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: [&#39;/((?!api/|_next/|favicon.ico|public/).*)&#39;,],
};
</code></pre>
<hr>
<h2 id="using-in-your-sitecore-components">Using in Your Sitecore Components</h2>
<h3 id="client-side-react-components">Client-Side (React Components)</h3>
<pre><code class="language-typescript">import { useSession } from &#39;next-auth/react&#39;;

export function MyAccountComponent() {
  const { data: session, status } = useSession();

  if (status === &#39;loading&#39;) return &lt;div&gt;Loading...&lt;/div&gt;;
  if (status === &#39;unauthenticated&#39;) return &lt;div&gt;Please sign in&lt;/div&gt;;

  return (
    &lt;div&gt;
      &lt;h1&gt;Welcome, {session?.user?.name}&lt;/h1&gt;
      &lt;p&gt;User ID: {session?.cardNumber}&lt;/p&gt;
      &lt;p&gt;Email: {session?.user?.email}&lt;/p&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="server-side-sitecore-api-routes">Server-Side (Sitecore API Routes)</h3>
<pre><code class="language-typescript">import { getServerSession } from &#39;next-auth/next&#39;;
import { authOptions } from &#39;./auth/[...nextauth]&#39;;
import type { NextApiRequest, NextApiResponse } from &#39;next&#39;;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const session = await getServerSession(req, res, authOptions);

  if (!session) {
    return res.status(401).json({ error: &#39;Unauthorized&#39; });
  }

  // Use session.gigyaToken for SAP CDC API calls
  // Use session.cardNumber for Sitecore personalization

  return res.status(200).json({ message: &#39;Protected data&#39;, user: session.user });
}
</code></pre>
<h3 id="sitecore-page-props">Sitecore Page Props</h3>
<pre><code class="language-typescript">import { getServerSession } from &#39;next-auth/next&#39;;
import { authOptions } from &#39;./api/auth/[...nextauth]&#39;;

export async function getServerSideProps(context) {
  const session = await getServerSession(context.req, context.res, authOptions);

  if (!session) {
    return {
      redirect: { destination: &#39;/login&#39;, permanent: false },
    };
  }

  return {
    props: {
      session,
      // Pass to Sitecore components
    },
  };
}
</code></pre>
<h2 id="common-issues-and-fixes">Common Issues and Fixes</h2>
<h3 id="failed-to-load-sap-cdc-sdk">&quot;Failed to load SAP CDC SDK&quot;</h3>
<ul>
<li><strong>Cause</strong>: Incorrect CDN URL or API key</li>
<li><strong>Fix</strong>: Check <code>NEXT_PUBLIC_GIGYA_CDN_BASE_URL</code> matches your data center (us1/eu1/au1)</li>
<li><strong>Fix</strong>: Verify API key is correct in SAP CDC Console</li>
</ul>
<h3 id="jwt-verification-failed">&quot;JWT verification failed&quot;</h3>
<ul>
<li><strong>Cause</strong>: Public key fetch failure or token expiration</li>
<li><strong>Fix</strong>: Ensure <code>NEXT_PUBLIC_GIGYA_API_KEY</code> is correct</li>
<li><strong>Fix</strong>: Verify data center URLs match your SAP CDC account region</li>
<li><strong>Fix</strong>: Check token hasn&#39;t expired (default: 10 minutes)</li>
</ul>
<h3 id="session-not-persisting">&quot;Session not persisting&quot;</h3>
<ul>
<li><strong>Cause</strong>: <code>NEXTAUTH_URL</code> mismatch or missing secret</li>
<li><strong>Fix</strong>: Verify <code>NEXTAUTH_URL</code> matches your domain exactly</li>
<li><strong>Fix</strong>: Ensure <code>NEXTAUTH_SECRET</code> is set and consistent across instances</li>
<li><strong>Fix</strong>: Check browser cookies are enabled</li>
</ul>
<h3 id="screen-set-not-displaying">&quot;Screen-set not displaying&quot;</h3>
<ul>
<li><strong>Cause</strong>: SDK not loaded or incorrect screen-set name</li>
<li><strong>Fix</strong>: Confirm screen-set name in SAP CDC Console matches <code>NEXT_PUBLIC_GIGYA_SCREENSET</code></li>
<li><strong>Fix</strong>: Check browser console for JavaScript errors</li>
<li><strong>Fix</strong>: Verify SDK loaded before calling <code>showScreenSet()</code></li>
</ul>
<h3 id="race-condition-gigya-is-not-defined">&quot;Race condition: gigya is not defined&quot;</h3>
<ul>
<li><strong>Cause</strong>: Using SDK before it&#39;s loaded</li>
<li><strong>Fix</strong>: Always use <code>loadGigyaScript()</code> Promise before accessing <code>window.gigya</code></li>
<li><strong>Fix</strong>: Check SDK loading status before making API calls</li>
</ul>
<hr>
<h2 id="security-best-practices">Security Best Practices</h2>
<h3 id="essential-security-checklist">Essential Security Checklist</h3>
<p>✅ <strong>Environment Variables</strong></p>
<ul>
<li>Add <code>.env.local</code> to <code>.gitignore</code></li>
<li>Use different secrets for dev/staging/production</li>
<li>Store production secrets in hosting platform (Vercel, Azure, etc.)</li>
</ul>
<p>✅ <strong>JWT Security</strong></p>
<ul>
<li>Tokens stored in HttpOnly cookies (XSS protection)</li>
<li>Short expiration times (30 minutes recommended)</li>
<li>Always verify tokens server-side</li>
</ul>
<p>✅ <strong>Session Management</strong></p>
<ul>
<li>Implement session timeout (default: 30 minutes)</li>
<li>Clear both SAP CDC and NextAuth sessions on logout</li>
<li>Validate sessions on every protected request</li>
</ul>
<p>✅ <strong>API Security</strong></p>
<pre><code class="language-typescript">// Always verify session server-side
const session = await getServerSession(req, res, authOptions);
if (!session) return res.status(401).json({ error: &#39;Unauthorized&#39; });
</code></pre>
<p>✅ <strong>CORS Configuration</strong></p>
<ul>
<li>Whitelist your domains in SAP CDC Console → Security → Trusted Sites</li>
<li>Never use wildcard (<code>*</code>) in production</li>
</ul>
<p>✅ <strong>HTTPS</strong></p>
<ul>
<li>Always use HTTPS in production</li>
<li>Secure cookies require HTTPS</li>
</ul>
<hr>
<h2 id="production-deployment">Production Deployment</h2>
<h3 id="sitecore-xm-cloud-deployment">Sitecore XM Cloud Deployment</h3>
<ol>
<li><strong>Add environment variables</strong> in XM Cloud portal or CI/CD pipeline</li>
<li><strong>Configure SAP CDC trusted sites</strong> with production URLs</li>
<li><strong>Update NEXTAUTH_URL</strong> to production domain</li>
<li><strong>Test authentication</strong> thoroughly before going live</li>
<li><strong>Enable HTTPS</strong> and secure cookies</li>
<li><strong>Monitor</strong> authentication logs and errors</li>
</ol>
<h3 id="vercel-deployment">Vercel Deployment</h3>
<pre><code class="language-bash">vercel env add NEXTAUTH_SECRET production
vercel env add GIGYA_USER_KEY production
vercel env add GIGYA_SECRET production
# Add all other secrets
</code></pre>
<h2 id="resources">Resources</h2>
<ul>
<li><strong>SAP CDC Documentation</strong>: <a href="https://help.sap.com/docs/SAP_CUSTOMER_DATA_CLOUD">help.sap.com/docs/SAP_CUSTOMER_DATA_CLOUD</a></li>
<li><strong>NextAuth.js</strong>: <a href="https://next-auth.js.org/">next-auth.js.org</a></li>
<li><strong>Sitecore JSS</strong>: <a href="https://doc.sitecore.com/xmc/en/developers/jss">doc.sitecore.com/xmc/en/developers/jss</a></li>
<li><strong>Sitecore XM Cloud</strong>: <a href="https://doc.sitecore.com/xmc">doc.sitecore.com/xmc</a></li>
</ul>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>Implementing SAP Customer Data Cloud authentication in Sitecore XM Cloud Next.js applications requires careful attention to JWT verification, session management, and SDK initialization. The challenges we faced—from dynamic public key fetching to session synchronization—are common in enterprise authentication implementations.</p>
<p>By following this guide and addressing the challenges we documented, you can build a production-ready authentication system that:</p>
<p>✅ <strong>Securely authenticates users</strong> with JWT-based tokens<br>✅ <strong>Integrates</strong> with Sitecore&#39;s headless architecture<br>✅ <strong>Maintains session state</strong> across page reloads and navigation<br>✅ <strong>Protects routes</strong> with middleware-based authentication<br>✅ <strong>Provides foundation</strong> for personalization with Sitecore Personalize  </p>
<p><strong>Remember</strong>: Always follow security best practices, keep dependencies updated, and test thoroughly in staging before production deployment. The authentication layer is critical—take time to get it right.</p>
<p>Happy coding! 🚀</p>
]]></content:encoded>
    </item>
    <item>
      <title>SEO Audit - Sitecore XM Cloud Marketplace App</title>
      <link>https://alikhan.tech/blogs/2025/seo-auditor-sitecore-marketplace-app</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/seo-auditor-sitecore-marketplace-app</guid>
      <pubDate>Sun, 12 Oct 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[Working in Sitecore XM Cloud, I often noticed a gap: it can be difficult to quickly identify SEO and accessibility issues on a website page. Things like missing alt text, page titles, or improper heading structures often slip through the cracks. These may seem like small…]]></description>
      <content:encoded><![CDATA[<p>Working in Sitecore XM Cloud, I often noticed a gap: it can be difficult to quickly identify SEO and accessibility issues on a website page. Things like missing alt text, page titles, or improper heading structures often slip through the cracks. These may seem like small details, but they matter a lot for SEO and accessibility.</p>
<p>To solve this, I built a Marketplace app that plugs directly into XM Cloud and lets you run a quick audit on published pages. The results show up instantly inside the Pages editor.</p>
<hr>
<h2 id="the-idea-behind-the-app">The Idea Behind the App</h2>
<p>The idea was simple. Instead of relying on external tools or waiting for SEO audits later, authors can now check their published pages right inside XM Cloud.</p>
<p>It makes the process faster, removes context switching, and gives immediate insights into metadata and accessibility.</p>
<hr>
<h2 id="how-the-app-works">How the App Works</h2>
<p>The app is built with Next.js and TypeScript and embedded into Sitecore Pages via the Marketplace. When opened, it fetches the published page’s rendered HTML and checks for:</p>
<ul>
<li>Missing alt attributes on images  </li>
<li>Missing or duplicate meta titles and descriptions  </li>
<li>Incorrect heading structure</li>
</ul>
<p>The results are displayed in a clean panel inside Pages.</p>
<h2 id="image"><img src="https://alikhan.tech/images/post/xmcma-1.png" alt="image"></h2>
<h2 id="creating-and-configuring-the-app-in-sitecore-xm-cloud">Creating and Configuring the App in Sitecore XM Cloud</h2>
<p>Here’s the exact process I followed to create and configure the app:</p>
<ol>
<li><strong>Build the app locally</strong></li>
</ol>
<pre><code class="language-bash">git clone https://github.com/mgnventures/xmc-page-seo-audit-app.git
cd xmc-page-seo-audit-app
npm install
npm run dev
</code></pre>
<p>The app runs on <code>http://localhost:3000</code>.</p>
<ol start="2">
<li><p><strong>Deploy the app</strong><br>Push it to Vercel, Netlify, or Azure.  </p>
</li>
<li><p><strong>Register the app in Sitecore XM Cloud</strong></p>
</li>
</ol>
<ul>
<li>Go to Sitecore Cloud Portal.  </li>
<li>Navigate to <strong>Cloud Portal → Developer Studio → Create App</strong>.</li>
</ul>
<p><img src="https://alikhan.tech/images/post/xmcma-2.png" alt="image"></p>
<ul>
<li><p>Fill in details:  </p>
<ul>
<li>Name  </li>
<li>App icon (from your repo’s public folder)  </li>
<li>App URL (your deployed endpoint)  </li>
<li>Select <strong>Pages Context Panel</strong> as the extension point</li>
</ul>
<p><img src="https://alikhan.tech/images/post/xmcma-3.png" alt="image"></p>
<p><img src="https://alikhan.tech/images/post/xmcma-4.png" alt="image"></p>
</li>
<li><p>Save the configuration.</p>
</li>
</ul>
<ol start="4">
<li><strong>Install the app in XM Cloud</strong></li>
</ol>
<ul>
<li>Open Sitecore Prtal.  </li>
<li>Go to the My Apps tab.  </li>
<li>Search for your app and click <strong>Install</strong>.</li>
</ul>
<p><img src="https://alikhan.tech/images/post/xmcma-5.png" alt="image"></p>
<ul>
<li>The app now appears as a panel inside Pages.</li>
</ul>
<hr>
<h2 id="real-use-cases">Real Use Cases</h2>
<ul>
<li>A content author can check if their published landing page has proper alt text.  </li>
<li>A marketer can confirm that the page has a correct title and description.  </li>
<li>A developer can extend the app with custom rules for their project.</li>
</ul>
<p>The biggest win is that you can run these checks on the live published page and see results immediately in XM Cloud.</p>
<hr>
<h2 id="getting-started-guide">Getting Started Guide</h2>
<p>For anyone who wants to try this app:</p>
<pre><code class="language-bash">git clone https://github.com/mgnventures/xmc-page-seo-audit-app.git
</code></pre>
<p>Then follow the steps above to register and configure the app in XM Cloud.</p>
<hr>
<h2 id="reflections-and-future-potential">Reflections and Future Potential</h2>
<p>Building this app showed me how easy it is to extend XM Cloud with custom utilities. The Marketplace is still new, and even small apps like this can add real value for authors and marketers.</p>
<p>Next, I would like to refine the audit rules and improve how results are displayed. I also hope other developers fork the project on GitHub and help make it more stable and useful:</p>
<p>👉 <a href="https://github.com/mgnventures/xmc-page-seo-audit-app">GitHub Repo – xmc-page-seo-audit-app</a></p>
<p>In the end, this app is about more than code—it’s about contributing something practical to the Sitecore community.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Secured Pages SXA Module for Next.js - Authentication Middleware Implementation</title>
      <link>https://alikhan.tech/blogs/2025/secured-pages-sxa-module-nextjs</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/secured-pages-sxa-module-nextjs</guid>
      <pubDate>Mon, 15 Sep 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[When building headless Sitecore implementations with Next.js, securing specific pages or sections of your website is a common requirement. Whether it's user profiles, admin areas, or premium content, you need a robust way to verify authentication and redirect unauthorized users…]]></description>
      <content:encoded><![CDATA[<p>When building headless Sitecore implementations with Next.js, securing specific pages or sections of your website is a common requirement. Whether it&#39;s user profiles, admin areas, or premium content, you need a robust way to verify authentication and redirect unauthorized users to a login page—without writing custom server-side code for each route.</p>
<p>This post walks through implementing a <strong>Secured Pages SXA Module</strong> using Next.js middleware architecture, similar to Sitecore&#39;s out-of-the-box redirect module. The solution provides a flexible, configuration-driven approach to protect pages based on path patterns stored in Sitecore.</p>
<hr>
<h2 id="the-challenge">The Challenge</h2>
<p>In headless Sitecore implementations, traditional server-side authentication checks aren&#39;t always feasible. You need:</p>
<ul>
<li><strong>Flexible path matching</strong>: Support for both exact paths (<code>/Profile</code>) and regex patterns (<code>/securedTree*</code>)</li>
<li><strong>Sitecore-driven configuration</strong>: Content authors should be able to configure secured paths without code deployments</li>
<li><strong>Middleware-based protection</strong>: Leverage Next.js middleware for efficient request interception</li>
<li><strong>GraphQL integration</strong>: Fetch security mappings from Sitecore&#39;s GraphQL endpoint</li>
<li><strong>Performance</strong>: Cache security mappings to avoid repeated API calls</li>
</ul>
<hr>
<h2 id="solution-overview">Solution Overview</h2>
<p>The Secured Pages SXA Module consists of three main components:</p>
<ol>
<li><strong>SecuredPagesMiddleware</strong>: Next.js middleware that intercepts requests and checks authentication</li>
<li><strong>GraphQLSecuredPagesService</strong>: Service that fetches security mappings from Sitecore via GraphQL</li>
<li><strong>Sitecore Content Structure</strong>: Templates and items that store the security configuration</li>
</ol>
<h3 id="architecture-flow">Architecture Flow</h3>
<pre><code>Request → Middleware → Check Authentication Token
                          ↓
                    Is Authorized?
                    /           \
                  Yes            No
                   ↓              ↓
            Allow Request    Check Sitecore Mappings
                                  ↓
                          Path Matches Secured Pattern?
                          /                    \
                        Yes                     No
                         ↓                       ↓
                  Redirect to Login        Allow Request
</code></pre>
<hr>
<h2 id="sitecore-setup">Sitecore Setup</h2>
<h3 id="step-1-create-the-secured-pages-template">Step 1: Create the Secured Pages Template</h3>
<p>First, create a template in Sitecore to store security mappings:</p>
<ol>
<li><p><strong>Navigate</strong> to <code>/sitecore/templates/Project/YourProject/Security</code></p>
</li>
<li><p><strong>Create a new template</strong> named <code>Secured Pages</code></p>
</li>
<li><p><strong>Add the following fields</strong>:</p>
<ul>
<li><p><strong>UrlMapping</strong> (Multi-Line Text)</p>
<ul>
<li>Description: &quot;Enter relative paths or regex patterns (one per line). Examples: <code>/Profile</code>, <code>/securedTree*</code>&quot;</li>
</ul>
</li>
<li><p><strong>LoginRedirectUrl</strong> (Single-Line Text)</p>
<ul>
<li>Description: &quot;Relative URL to redirect unauthorized users (e.g., <code>/login</code>)&quot;</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Set the template ID</strong> and note it for use in your constants file:</p>
</li>
</ol>
<pre><code class="language-typescript">// lib/constants.ts
export const securedPagesTemplateId = &#39;{YOUR-TEMPLATE-ID-HERE}&#39;;
</code></pre>
<h3 id="step-2-create-security-mappings-container">Step 2: Create Security Mappings Container</h3>
<ol>
<li><strong>Navigate</strong> to your site&#39;s root item (e.g., <code>/sitecore/content/YourSite</code>)</li>
<li><strong>Create a folder</strong> named <code>Settings</code> (if it doesn&#39;t exist)</li>
<li><strong>Under Settings</strong>, create a folder named <code>Security Mappings</code></li>
<li><strong>Set insert options</strong> on <code>Security Mappings</code> to allow <code>Secured Pages</code> template</li>
</ol>
<h3 id="step-3-create-security-mapping-items">Step 3: Create Security Mapping Items</h3>
<p>For each set of secured paths:</p>
<ol>
<li><strong>Navigate</strong> to <code>Settings/Security Mappings</code></li>
<li><strong>Insert</strong> → <code>Secured Pages</code></li>
<li><strong>Configure</strong>:<ul>
<li><strong>UrlMapping</strong>: Enter paths (one per line):<pre><code>/Profile
/Account/*
/Admin/.*
</code></pre>
</li>
<li><strong>LoginRedirectUrl</strong>: <code>/login</code></li>
</ul>
</li>
</ol>
<hr>
<h2 id="code-implementation">Code Implementation</h2>
<h3 id="step-1-create-constants-file">Step 1: Create Constants File</h3>
<pre><code class="language-typescript">// lib/constants.ts
export const securedPagesTemplateId = &#39;{YOUR-TEMPLATE-ID-HERE}&#39;;
</code></pre>
<h3 id="step-2-graphql-service-implementation">Step 2: GraphQL Service Implementation</h3>
<p>The <code>GraphQLSecuredPagesService</code> fetches security mappings from Sitecore:</p>
<pre><code class="language-typescript">// lib/middleware/graphql-auth-service.ts
import {
  GraphQLRequestClient,
  GraphQLRequestClientFactory,
} from &#39;@sitecore-jss/sitecore-jss-nextjs/graphql&#39;;
import { securedPagesTemplateId } from &#39;../constants&#39;;

export const GetItemUrl = /* GraphQL */ `
  query GetItemIdByPath($path: String, $lang: String = &quot;en&quot;) {
    item(path: $path, language: $lang) {
      id
    }
  }
`;

const SiteRootQuery = /* GraphQL */ `
  query SiteRootQuery($siteName: String!) {
    site {
      siteInfo(site: $siteName) {
        rootPath
      }
    }
  }
`;

const defaultQuery = /* GraphQL */ `
  query SearchSecuredPages($pathId: String!, $templateId: String!) {
    search(
      where: {
        AND: [
          { name: &quot;_path&quot;, value: $pathId, operator: CONTAINS }
          { name: &quot;_templates&quot;, value: $templateId, operator: CONTAINS }
        ]
      }
      first: 1
    ) {
      total
      pageInfo {
        endCursor
        hasNext
      }
      results {
        UrlMapping: field(name: &quot;UrlMapping&quot;) {
          value
        }
        LoginRedirectUrl: field(name: &quot;LoginRedirectUrl&quot;) {
          value
        }
      }
    }
  }
`;

export type GraphQLSecuredPagesServiceConfig = {
  endpoint?: string;
  apiKey?: string;
  fetch?: typeof fetch;
  clientFactory?: GraphQLRequestClientFactory;
  cacheEnabled?: boolean;
  cacheTimeout?: number;
};

export type SecuredPagesQueryResult = {
  search: {
    total: number;
    results: resultEntry[] | null;
  };
};

export type resultEntry = {
  UrlMapping: resultField;
  LoginRedirectUrl: resultField;
};

export type resultField = {
  value: string;
};

type SiteRootQueryResult = {
  site: { siteInfo: { rootPath: string } | null };
};

type TItem = {
  id: string;
};

type TGetItemId = {
  item: TItem;
};

export class GraphQLSecuredPagesService {
  private graphQLClient: GraphQLRequestClient;
  private cache: Map&lt;string, { value: SecuredPagesQueryResult; expiresAt: number }&gt; = new Map();

  protected get siteRootQuery(): string {
    return SiteRootQuery;
  }

  protected get query(): string {
    return defaultQuery;
  }

  constructor(private options: GraphQLSecuredPagesServiceConfig) {
    this.graphQLClient = this.getGraphQLClient();
  }

  async fetchSecuredPages({ siteName }: { siteName: string }): Promise&lt;SecuredPagesQueryResult&gt; {
    if (!siteName) {
      throw new Error(&#39;The siteName cannot be empty&#39;);
    }

    // Check cache first
    const cacheKey = `auth-${siteName}`;
    const cached = this.cache.get(cacheKey);
    
    if (cached &amp;&amp; Date.now() &lt; cached.expiresAt) {
      return cached.value;
    }

    // Fetch from Sitecore
    const siteRoot = await this.graphQLClient.request&lt;SiteRootQueryResult&gt;(this.siteRootQuery, {
      siteName,
    });
    
    if (!siteRoot?.site?.siteInfo?.rootPath) {
      return { search: { total: 0, results: [] } };
    }

    const settingItemPath = siteRoot.site.siteInfo.rootPath + &#39;/Settings&#39;;
    const settingItemData = await this.graphQLClient.request&lt;TGetItemId&gt;(GetItemUrl, {
      path: settingItemPath,
    });
    
    if (!settingItemData?.item?.id) {
      return { search: { total: 0, results: [] } };
    }

    const pathId = settingItemData.item.id;
    const securedPages = await this.graphQLClient.request&lt;SecuredPagesQueryResult&gt;(
      this.query,
      {
        pathId,
        templateId: securedPagesTemplateId,
      }
    );
    
    // Cache the result
    if (securedPages) {
      const cacheTimeout = (this.options.cacheTimeout || 10) * 1000;
      this.cache.set(cacheKey, {
        value: securedPages,
        expiresAt: Date.now() + cacheTimeout,
      });
      return securedPages;
    }
    
    return { search: { total: 0, results: [] } };
  }

  protected getGraphQLClient(): GraphQLRequestClient {
    if (!this.options.endpoint) {
      if (!this.options.clientFactory) {
        throw new Error(&#39;You should provide either an endpoint and apiKey, or a clientFactory.&#39;);
      }

      return this.options.clientFactory({
        fetch: this.options.fetch,
      });
    }

    return new GraphQLRequestClient(this.options.endpoint, {
      apiKey: this.options.apiKey,
      fetch: this.options.fetch,
    });
  }
}
</code></pre>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Caching</strong>: Uses in-memory cache to avoid repeated GraphQL calls (default: 10 seconds)</li>
<li><strong>Path Resolution</strong>: Dynamically resolves the site&#39;s Settings folder path</li>
<li><strong>Template Filtering</strong>: Searches for items using the <code>Secured Pages</code> template ID</li>
</ul>
<p><strong>Note:</strong> The caching implementation uses a simple in-memory Map. For production environments with multiple instances, consider implementing a distributed cache solution.</p>
<h3 id="step-3-middleware-implementation">Step 3: Middleware Implementation</h3>
<p>The middleware intercepts requests and enforces authentication:</p>
<pre><code class="language-typescript">// lib/middleware/secured-pages-middleware.ts
import { SiteInfo } from &#39;@sitecore-jss/sitecore-jss-nextjs&#39;;
import { MiddlewareBase, MiddlewareBaseConfig } from &#39;@sitecore-jss/sitecore-jss-nextjs/middleware&#39;;
import { getToken } from &#39;next-auth/jwt&#39;;
import { NextRequest, NextResponse } from &#39;next/server&#39;;
import regexParser from &#39;regex-parser&#39;;
import {
  GraphQLSecuredPagesService,
  GraphQLSecuredPagesServiceConfig,
  resultEntry,
} from &#39;./graphql-auth-service&#39;;

export type SecuredPagesMiddlewareConfig = Omit&lt;GraphQLSecuredPagesServiceConfig, &#39;fetch&#39;&gt; &amp;
  MiddlewareBaseConfig &amp; {
    protectedRoutes?: string[];
    defaultLoginPage?: string;
    locales: string[];
  };

export class SecuredPagesMiddleware extends MiddlewareBase {
  private securedPagesService: GraphQLSecuredPagesService;

  constructor(protected config: SecuredPagesMiddlewareConfig) {
    super(config);
    this.securedPagesService = new GraphQLSecuredPagesService({ ...config, fetch: fetch });
  }

  public getHandler(): (req: NextRequest, res?: NextResponse) =&gt; Promise&lt;NextResponse&gt; {
    return async (req, res) =&gt; {
      return await this.handler(req, res);
    };
  }

  private handler = async (req: NextRequest, res?: NextResponse): Promise&lt;NextResponse&gt; =&gt; {
    // Skip if disabled or in preview mode
    if (
      (this.config.disabled &amp;&amp; this.config.disabled(req, res || NextResponse.next())) ||
      this.isPreview(req) ||
      this.excludeRoute(req.nextUrl.pathname)
    ) {
      return res || NextResponse.next();
    }

    const site = this.getSite(req, res);
    const isSecured = this.config.protectedRoutes?.includes(req.nextUrl.pathname);
    const existsSecuredPageMapping = await this.getExistsSecuredPages(req, site.name);

    // Allow if not secured
    if (!isSecured &amp;&amp; !existsSecuredPageMapping) {
      return res || NextResponse.next();
    }

    // Check authorization
    if (await this.isAuthorized(req)) {
      return res || NextResponse.next();
    }

    // Redirect to login
    const url = req.nextUrl.clone();
    url.pathname =
      existsSecuredPageMapping?.LoginRedirectUrl?.value || this.config.defaultLoginPage || &#39;/&#39;;
    url.locale = req.nextUrl.locale;

    return NextResponse.redirect(decodeURIComponent(url.href));
  };

  private async isAuthorized(req: NextRequest): Promise&lt;boolean&gt; {
    const secret = process.env.NEXTAUTH_JWT_SECRET;
    const token = await getToken({ req, secret });
    return !!token;
  }

  private async getExistsSecuredPages(
    req: NextRequest,
    siteName: string
  ): Promise&lt;resultEntry | undefined&gt; {
    const securedPages = await this.securedPagesService.fetchSecuredPages({ siteName });

    if (!securedPages?.search?.results || securedPages.search.total === 0) {
      return undefined;
    }

    const pathname = req.nextUrl.pathname.toLowerCase();
    const pathnameWithLocale = `/${req.nextUrl.locale}${req.nextUrl.pathname}`.toLowerCase();

    // Check each secured page mapping
    for (const result of securedPages.search.results) {
      const urlMappings = result?.UrlMapping?.value;
      if (!urlMappings) continue;

      const paths = urlMappings.split(/\r?\n/);
      
      for (const urlMapping of paths) {
        if (!urlMapping.trim()) continue;

        const pattern = regexParser(urlMapping.toLowerCase());
        const matches = pattern.test(pathname) || pattern.test(pathnameWithLocale);

        if (matches &amp;&amp; result.LoginRedirectUrl) {
          return result;
        }
      }
    }

    return undefined;
  }
}
</code></pre>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Token Verification</strong>: Uses <code>next-auth/jwt</code> to verify authentication tokens</li>
<li><strong>Path Matching</strong>: Supports both exact paths and regex patterns</li>
<li><strong>Locale Awareness</strong>: Matches paths with and without locale prefixes</li>
<li><strong>Preview Mode</strong>: Skips authentication checks in Sitecore preview mode</li>
<li><strong>Configurable Routes</strong>: Supports both Sitecore-driven and code-based protected routes</li>
</ul>
<hr>
<h2 id="configuration">Configuration</h2>
<h3 id="environment-variables">Environment Variables</h3>
<p>Add to your <code>.env</code> file:</p>
<pre><code class="language-bash"># Enable/disable the secured pages module
Enable_Auth_Secured_Pages=true

# NextAuth JWT secret (required for token verification)
NEXTAUTH_JWT_SECRET=your-secret-key-here
</code></pre>
<h3 id="middleware-integration">Middleware Integration</h3>
<p>Integrate the middleware in your Next.js middleware file:</p>
<pre><code class="language-typescript">// middleware.ts
import { NextRequest, NextResponse } from &#39;next/server&#39;;
import { createGraphQLClientFactory } from &#39;@sitecore-jss/sitecore-jss-nextjs&#39;;
import { SecuredPagesMiddleware } from &#39;./lib/middleware/secured-pages-middleware&#39;;

const securedPagesMiddleware = new SecuredPagesMiddleware({
  // GraphQL configuration
  clientFactory: createGraphQLClientFactory({
    endpoint: process.env.GRAPH_QL_ENDPOINT!,
    apiKey: process.env.SITECORE_API_KEY!,
  }),

  // Middleware configuration
  defaultHostname: process.env.SITECORE_DEFAULT_HOSTNAME || &#39;localhost&#39;,
  locales: [&#39;en&#39;, &#39;es&#39;, &#39;fr&#39;], // Add your locales
  defaultLoginPage: &#39;/login&#39;,
  
  // Optional: Code-based protected routes (in addition to Sitecore config)
  protectedRoutes: [&#39;/admin&#39;, &#39;/dashboard&#39;],
  
  // Cache configuration
  cacheEnabled: true,
  cacheTimeout: 10, // seconds

  // Exclude routes from authentication checks
  excludeRoute: (pathname: string) =&gt; {
    return pathname.startsWith(&#39;/api&#39;) || pathname.startsWith(&#39;/_next&#39;);
  },

  // Disable middleware conditionally
  disabled: (req: NextRequest) =&gt; {
    return process.env.Enable_Auth_Secured_Pages !== &#39;true&#39;;
  },
});

export async function middleware(req: NextRequest) {
  // Run other middlewares first (e.g., Sitecore JSS middleware)
  // Then run secured pages middleware
  return securedPagesMiddleware.getHandler()(req);
}

export const config = {
  matcher: [
    &#39;/((?!api|_next/static|_next/image|favicon.ico).*)&#39;,
  ],
};
</code></pre>
<hr>
<h2 id="step-by-step-setup-guide">Step-by-Step Setup Guide</h2>
<h3 id="1-install-dependencies">1. Install Dependencies</h3>
<pre><code class="language-bash">npm install regex-parser next-auth
</code></pre>
<h3 id="2-create-file-structure">2. Create File Structure</h3>
<pre><code>lib/
  ├── constants.ts
  └── middleware/
      ├── secured-pages-middleware.ts
      └── graphql-auth-service.ts
</code></pre>
<h3 id="3-configure-sitecore">3. Configure Sitecore</h3>
<ol>
<li>Create the <code>Secured Pages</code> template with <code>UrlMapping</code> and <code>LoginRedirectUrl</code> fields</li>
<li>Create <code>Settings/Security Mappings</code> folder under your site root</li>
<li>Create security mapping items as needed</li>
</ol>
<h3 id="4-update-environment-variables">4. Update Environment Variables</h3>
<p>Add <code>Enable_Auth_Secured_Pages</code> and <code>NEXTAUTH_JWT_SECRET</code> to your <code>.env</code> file.</p>
<h3 id="5-integrate-middleware">5. Integrate Middleware</h3>
<p>Add the middleware configuration to <code>middleware.ts</code> as shown above.</p>
<h3 id="6-test-the-implementation">6. Test the Implementation</h3>
<ol>
<li><strong>Test Unauthorized Access</strong>: Visit a secured path without authentication → should redirect to login</li>
<li><strong>Test Authorized Access</strong>: Login and visit secured path → should allow access</li>
<li><strong>Test Regex Patterns</strong>: Verify that patterns like <code>/securedTree*</code> match correctly</li>
</ol>
<hr>
<h2 id="advanced-configuration">Advanced Configuration</h2>
<h3 id="multiple-security-mappings">Multiple Security Mappings</h3>
<p>You can create multiple <code>Secured Pages</code> items under <code>Security Mappings</code>, each with different path patterns and redirect URLs. The middleware will check all mappings and use the first match.</p>
<h3 id="combining-code-based-and-sitecore-based-routes">Combining Code-Based and Sitecore-Based Routes</h3>
<p>The middleware supports both approaches:</p>
<ul>
<li><strong>Code-based</strong>: Define <code>protectedRoutes</code> in middleware config</li>
<li><strong>Sitecore-based</strong>: Configure paths in Sitecore items</li>
</ul>
<p>Both are checked, and either can trigger authentication.</p>
<h3 id="custom-authentication-logic">Custom Authentication Logic</h3>
<p>To use a different authentication provider, modify the <code>isAuthorized</code> method:</p>
<pre><code class="language-typescript">private async isAuthorized(req: NextRequest): Promise&lt;boolean&gt; {
  // Example: Check custom session cookie
  const sessionCookie = req.cookies.get(&#39;session&#39;);
  return !!sessionCookie?.value;
  
  // Or: Check custom header
  const authHeader = req.headers.get(&#39;Authorization&#39;);
  return authHeader?.startsWith(&#39;Bearer &#39;) ?? false;
}
</code></pre>
<hr>
<h2 id="performance-considerations">Performance Considerations</h2>
<ul>
<li><strong>Caching</strong>: Security mappings are cached for 10 seconds by default. Adjust <code>cacheTimeout</code> based on your needs.</li>
<li><strong>GraphQL Efficiency</strong>: The service uses a single GraphQL query to fetch all mappings for a site.</li>
<li><strong>Middleware Overhead</strong>: Middleware runs on every request. Use <code>excludeRoute</code> to skip static assets and API routes.</li>
</ul>
<hr>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="mappings-not-found">Mappings Not Found</h3>
<ul>
<li>Verify the template ID in <code>constants.ts</code> matches your Sitecore template</li>
<li>Check that items exist under <code>Settings/Security Mappings</code></li>
<li>Ensure the <code>UrlMapping</code> field is populated</li>
</ul>
<h3 id="redirects-not-working">Redirects Not Working</h3>
<ul>
<li>Verify <code>LoginRedirectUrl</code> is set in Sitecore items</li>
<li>Check that <code>defaultLoginPage</code> is configured in middleware</li>
<li>Ensure the login page route exists</li>
</ul>
<h3 id="authentication-not-detected">Authentication Not Detected</h3>
<ul>
<li>Verify <code>NEXTAUTH_JWT_SECRET</code> is set correctly</li>
<li>Check that your authentication provider sets tokens correctly</li>
</ul>
<hr>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>The Secured Pages SXA Module provides a flexible, maintainable solution for protecting pages in headless Sitecore implementations. By leveraging Next.js middleware and Sitecore&#39;s content management capabilities, you can:</p>
<ul>
<li><strong>Empower content authors</strong> to configure security without code changes</li>
<li><strong>Maintain performance</strong> through intelligent caching</li>
<li><strong>Support complex scenarios</strong> with regex pattern matching</li>
<li><strong>Integrate seamlessly</strong> with existing authentication providers</li>
</ul>
<p>This approach follows Sitecore best practices by keeping configuration in the CMS while leveraging Next.js&#39;s powerful middleware capabilities for request handling.</p>
<p><strong>Ready to secure your headless site?</strong><br>👉 <em>Follow the step-by-step guide above to implement the module in your Next.js Sitecore project.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>GraphQL Query Optimization in Sitecore XM Cloud - Performance Patterns for Enterprise Scale</title>
      <link>https://alikhan.tech/blogs/2025/graphql-optimization-sitecore-xm-cloud</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/graphql-optimization-sitecore-xm-cloud</guid>
      <pubDate>Wed, 20 Aug 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[GraphQL performance is one of the most common pain points in Sitecore XM Cloud implementations. Developers report page load times degrading under traffic, inconsistent Experience Edge cache behavior, and unexpectedly high API costs. The problem often isn't GraphQL itself—it's…]]></description>
      <content:encoded><![CDATA[<p>GraphQL performance is one of the most common pain points in Sitecore XM Cloud implementations. Developers report page load times degrading under traffic, inconsistent Experience Edge cache behavior, and unexpectedly high API costs. The problem often isn&#39;t GraphQL itself—it&#39;s how queries are structured.</p>
<p>This guide provides enterprise-grade optimization patterns based on real production implementations. You&#39;ll learn concrete techniques to reduce query complexity, eliminate N+1 problems, and leverage Experience Edge caching effectively.</p>
<hr>
<h2 id="⚠️-the-problem-whats-killing-your-graphql-performance">⚠️ The Problem: What&#39;s Killing Your GraphQL Performance</h2>
<h3 id="common-issues">Common Issues</h3>
<p><strong>Over-fetching</strong>: Requesting more fields than needed, increasing payload size and processing time.</p>
<p><strong>N+1 Query Problem</strong>: Making sequential queries instead of batching, multiplying API calls.</p>
<p><strong>Complex Nested Queries</strong>: Deeply nested field resolution triggering expensive data lookups.</p>
<p><strong>Cache Misses</strong>: Not leveraging Experience Edge caching, causing repeated expensive queries.</p>
<p><strong>Inefficient Fragments</strong>: Not reusing query fragments, duplicating field definitions.</p>
<h3 id="real-world-impact">Real-World Impact</h3>
<ul>
<li>Page load time: 2-5 seconds (should be less than 500ms)</li>
<li>API latency: 500ms-2s per request (should be less than 100ms)</li>
<li>Network bandwidth: 2-5MB per page load (should be less than 200KB)</li>
<li>Experience Edge cache hit rate: 30-40% (should be greater than 90%)</li>
</ul>
<hr>
<h2 id="🧩-solution-1-graphql-fragments-for-reusability">🧩 Solution 1: GraphQL Fragments for Reusability</h2>
<p>Use fragments to define reusable field sets and avoid duplication.</p>
<pre><code class="language-graphql">fragment ComponentFieldsFragment on Component {
  id
  name
  displayName
  fields {
    name
    value
  }
}

query GetPages {
  search(where: { AND: [{ name: &quot;_templatename&quot;, value: &quot;Page&quot; }] }) {
    results {
      ...ComponentFieldsFragment
      children {
        ...ComponentFieldsFragment
      }
    }
  }
}
</code></pre>
<p><strong>Benefits:</strong></p>
<ul>
<li>Reduced query size by 30-40%</li>
<li>Easier to maintain field lists across queries</li>
<li>Better cache utilization</li>
</ul>
<hr>
<h2 id="🔄-solution-2-batching-with-aliases">🔄 Solution 2: Batching with Aliases</h2>
<p>Instead of multiple sequential queries, use aliases to batch requests into a single GraphQL call.</p>
<pre><code class="language-graphql">query BatchedQuery {
  header: item(path: &quot;/sitecore/content/home/header&quot;) {
    id
    name
  }
  footer: item(path: &quot;/sitecore/content/home/footer&quot;) {
    id
    name
  }
  navigation: item(path: &quot;/sitecore/content/home/navigation&quot;) {
    id
    name
  }
}
</code></pre>
<p><strong>Impact:</strong></p>
<ul>
<li>Reduces API calls from 3 to 1</li>
<li>Single network round-trip vs. three</li>
<li>Faster overall page rendering</li>
</ul>
<hr>
<h2 id="📄-solution-3-cursor-based-pagination">📄 Solution 3: Cursor-Based Pagination</h2>
<p>Avoid loading all items at once. Use cursor-based pagination for large datasets.</p>
<pre><code class="language-graphql">query PaginatedSearch($first: Int!, $after: String) {
  search(
    where: { AND: [{ name: &quot;_templatename&quot;, value: &quot;BlogPost&quot; }] }
    first: $first
    after: $after
  ) {
    pageInfo {
      hasNextPage
      endCursor
    }
    results {
      id
      name
      url
      createdDate
    }
  }
}
</code></pre>
<p><strong>Variables:</strong></p>
<pre><code class="language-json">{
  &quot;first&quot;: 20,
  &quot;after&quot;: null
}
</code></pre>
<p><strong>Benefits:</strong></p>
<ul>
<li>Load only what&#39;s needed</li>
<li>Faster initial page loads</li>
<li>Better memory usage</li>
</ul>
<hr>
<h2 id="💾-solution-4-experience-edge-cache-configuration">💾 Solution 4: Experience Edge Cache Configuration</h2>
<p>Experience Edge caches layout data automatically, but only if you configure it correctly.</p>
<pre><code class="language-typescript">// next.config.js
module.exports = {
  revalidate: 60, // ISR: revalidate every 60 seconds
  headers: async () =&gt; {
    return [
      {
        source: &#39;/api/graphql&#39;,
        headers: [
          {
            key: &#39;Cache-Control&#39;,
            value: &#39;public, s-maxage=300, stale-while-revalidate=600&#39;
          }
        ]
      }
    ];
  }
};
</code></pre>
<p><strong>Cache Hierarchy:</strong></p>
<ol>
<li>CDN cache (Experience Edge) - 5 minutes</li>
<li>Browser cache - 10 minutes</li>
<li>Application cache - In-memory storage</li>
</ol>
<hr>
<h2 id="🧮-solution-5-query-complexity-analysis">🧮 Solution 5: Query Complexity Analysis</h2>
<p>Implement query complexity scoring to reject expensive queries before execution.</p>
<pre><code class="language-typescript">// Apollo Server with complexity plugin
import { createComplexityLimitPlugin } from &#39;graphql-query-complexity&#39;;

const server = new ApolloServer({
  schema,
  plugins: [
    createComplexityLimitPlugin({
      maxComplexity: 1000,
      onComplete: (complexity) =&gt; {
        console.log(`Query complexity: ${complexity}`);
      }
    })
  ]
});
</code></pre>
<p><strong>Complexity Scoring Guide:</strong></p>
<ul>
<li>Simple field: 1 point</li>
<li>Relational field: 5 points</li>
<li>Collection: 10 points per item</li>
<li>Nested relations: Multiplicative</li>
</ul>
<hr>
<h2 id="⚡-solution-6-dataloader-for-batch-processing">⚡ Solution 6: DataLoader for Batch Processing</h2>
<p>Prevent N+1 problems by batching database lookups automatically.</p>
<pre><code class="language-typescript">import DataLoader from &#39;dataloader&#39;;

const componentLoader = new DataLoader(async (componentIds) =&gt; {
  return await Promise.all(
    componentIds.map(id =&gt; getComponentFromCache(id))
  );
});

// In resolver
const resolveComponent = async (parent, args) =&gt; {
  return componentLoader.load(args.componentId);
};
</code></pre>
<p><strong>Benefits:</strong></p>
<ul>
<li>Automatically batches queries</li>
<li>Single DB round-trip for multiple items</li>
<li>Reduces latency by 50-70%</li>
</ul>
<hr>
<h2 id="✅-performance-checklist">✅ Performance Checklist</h2>
<ul>
<li><input disabled="" type="checkbox"> Use GraphQL fragments for all field sets</li>
<li><input disabled="" type="checkbox"> Batch queries with aliases where possible</li>
<li><input disabled="" type="checkbox"> Implement cursor-based pagination for collections</li>
<li><input disabled="" type="checkbox"> Configure ISR revalidation times appropriately</li>
<li><input disabled="" type="checkbox"> Set correct Cache-Control headers</li>
<li><input disabled="" type="checkbox"> Monitor query complexity</li>
<li><input disabled="" type="checkbox"> Use DataLoader in resolvers</li>
<li><input disabled="" type="checkbox"> Profile queries in production</li>
<li><input disabled="" type="checkbox"> Set up alerts for slow queries (greater than 500ms)</li>
<li><input disabled="" type="checkbox"> Review Experience Edge cache hit rates monthly</li>
</ul>
<hr>
<h2 id="📊-monitoring-and-measurement">📊 Monitoring and Measurement</h2>
<p>Track these metrics to measure optimization impact:</p>
<p><strong>Query Performance:</strong></p>
<ul>
<li>Average query time (target: less than 100ms)</li>
<li>P95 query time (target: less than 500ms)</li>
<li>Queries per second (baseline)</li>
</ul>
<p><strong>Cache Effectiveness:</strong></p>
<ul>
<li>Experience Edge hit rate (target: greater than 90%)</li>
<li>Cache size (monitor for bloat)</li>
<li>Stale content incidents</li>
</ul>
<p><strong>Business Impact:</strong></p>
<ul>
<li>Page load time</li>
<li>Core Web Vitals (LCP, FID, CLS)</li>
<li>Bounce rate</li>
</ul>
<hr>
<h2 id="🛠️-tools-for-analysis">🛠️ Tools for Analysis</h2>
<p><strong>Apollo DevTools</strong>: Browser extension showing query execution details</p>
<p><strong>GraphQL Analyzer</strong>: Query complexity and performance scoring</p>
<p><strong>Experience Edge Insights</strong>: Built-in Sitecore caching dashboard</p>
<p><strong>Lighthouse</strong>: Core Web Vitals measurement</p>
<hr>
<h2 id="🎯-conclusion">🎯 Conclusion</h2>
<p>GraphQL performance optimization isn&#39;t a one-time effort—it&#39;s a continuous process. Start with query fragments and batching, then progressively implement caching and complexity controls. Monitor regularly and adjust based on production metrics.</p>
<p>The techniques in this guide can reduce API latency by 60-80% and improve page load times significantly. Apply them incrementally and measure the impact on your specific workload.</p>
<hr>
]]></content:encoded>
    </item>
    <item>
      <title>Setting Up Local HTTPS with Caddy Reverse Proxy for Next.js JSS</title>
      <link>https://alikhan.tech/blogs/2025/setting-up-local-HTTPS-with-caddy</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/setting-up-local-HTTPS-with-caddy</guid>
      <pubDate>Sat, 12 Jul 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[Running your Next.js + Sitecore JSS stack under HTTPS locally isn’t just about flashing a padlock icon—it keeps API contracts, cookies, and OAuth flows behaving exactly like they do in production. Below is the full workflow I use to stand up https://localdev.corp.test on Windows…]]></description>
      <content:encoded><![CDATA[<p>Running your <strong>Next.js + Sitecore JSS</strong> stack under HTTPS locally isn’t just about flashing a padlock icon—it keeps API contracts, cookies, and OAuth flows behaving exactly like they do in production. Below is the full workflow I use to stand up <code>https://localdev.corp.test</code> on Windows with <strong>Caddy</strong> and <strong>mkcert</strong>. Swap in your own domain as needed.</p>
<hr>
<h2 id="🧩-prerequisites">🧩 Prerequisites</h2>
<ul>
<li>Windows 10/11 with Administrator rights  </li>
<li>Chocolatey (Windows package manager)  </li>
<li>Node.js LTS, Caddy, mkcert, Git, VS Code</li>
</ul>
<blockquote>
<p>Already have Chocolatey and some tools? Great—just skip whatever’s installed and keep moving.</p>
</blockquote>
<hr>
<h2 id="📦-1-install-chocolatey">📦 1. Install Chocolatey</h2>
<p>Chocolatey makes the rest of the tooling painless.</p>
<ol>
<li>Launch <strong>PowerShell as Administrator</strong>.  </li>
<li>Run:<pre><code class="language-powershell">Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(&#39;https://community.chocolatey.org/install.ps1&#39;))
</code></pre>
</li>
<li>Confirm it worked:<pre><code class="language-powershell">choco -v
</code></pre>
</li>
</ol>
<hr>
<h2 id="🧱-2-install-your-toolchain">🧱 2. Install Your Toolchain</h2>
<p>With Chocolatey ready, pull down everything in one shot (still in elevated PowerShell):</p>
<pre><code class="language-powershell">choco install nodejs-lts mkcert caddy git vscode -y
</code></pre>
<p>Feel free to trim that list if you already have Git or VS Code.</p>
<hr>
<h2 id="🔒-3-generate-local-certificates">🔒 3. Generate Local Certificates</h2>
<p>Certs live alongside the project so Caddy can reference them.</p>
<pre><code class="language-powershell">cd C:\Projects\pennent-xm-cloud\gigya-local-setup
mkcert -install
mkcert localdev.corp.test
</code></pre>
<p>You’ll get:</p>
<ul>
<li><code>localdev.corp.test.pem</code>  </li>
<li><code>localdev.corp.test-key.pem</code></li>
</ul>
<p>Keep both inside <code>gigya-local-setup</code>.</p>
<hr>
<h2 id="🧠-4-map-the-domain-in-hosts">🧠 4. Map the Domain in <code>hosts</code></h2>
<p>Point your custom domain at localhost (IPv4 and IPv6).</p>
<ol>
<li>Open <strong>Notepad as Administrator</strong>.  </li>
<li>Edit <code>C:\Windows\System32\drivers\etc\hosts</code>.  </li>
<li>Append:<pre><code>127.0.0.1   localdev.corp.test
::1         localdev.corp.test
</code></pre>
</li>
<li>Save and flush DNS:<pre><code class="language-powershell">ipconfig /flushdns
</code></pre>
</li>
</ol>
<hr>
<h2 id="🛑-5-free-up-ports-80443">🛑 5. Free Up Ports 80/443</h2>
<p>Caddy won’t start if IIS (or anything else) has those ports locked.</p>
<pre><code class="language-powershell">Stop-Service W3SVC -Force
Set-Service W3SVC -StartupType Manual
netstat -ano | findstr &quot;:80 &quot;
netstat -ano | findstr &quot;:443 &quot;
</code></pre>
<p>If something’s still listening, track the PID and stop it:</p>
<pre><code class="language-powershell">Stop-Process -Id &lt;PID&gt; -Force
</code></pre>
<hr>
<h2 id="🧾-6-author-the-caddyfile">🧾 6. Author the <code>Caddyfile</code></h2>
<p>Create a file literally named <code>Caddyfile</code> in <code>C:\Projects\pennent-xm-cloud\gigya-local-setup</code>:</p>
<pre><code class="language-caddy">localdev.corp.test:443 {
    tls localdev.corp.test.pem localdev.corp.test-key.pem
    reverse_proxy localhost:3000
}
</code></pre>
<ul>
<li><code>localhost:3000</code> → where your Next.js/Sitecore JSS dev server runs  </li>
<li><code>localdev.corp.test</code> → the domain you mapped in <code>hosts</code></li>
</ul>
<hr>
<h2 id="🚀-7-run-caddy">🚀 7. Run Caddy</h2>
<p>From the same directory:</p>
<pre><code class="language-powershell">cd C:\Projects\pennent-xm-cloud\gigya-local-setup
caddy run --config Caddyfile
</code></pre>
<p>Look for a log line similar to:</p>
<pre><code>http.log        server running  {&quot;name&quot;: &quot;srv0&quot;, &quot;protocols&quot;: [&quot;h1&quot;, &quot;h2&quot;, &quot;h3&quot;]}
</code></pre>
<p>That’s Caddy reverse-proxying HTTPS traffic to your dev server.</p>
<hr>
<h2 id="✅-8-test-the-site">✅ 8. Test the Site</h2>
<p>Fire up your Next.js app (<code>npm run dev</code>) and load <code>https://localdev.corp.test</code> in the browser. You should see:</p>
<ul>
<li>Your local Sitecore JSS experience  </li>
<li>A trusted certificate (padlock + “Connection is secure”)</li>
</ul>
<hr>
<h2 id="🧹-9-optional-format-the-caddyfile">🧹 9. Optional: Format the <code>Caddyfile</code></h2>
<p>Caddy can tidy and lint its own config:</p>
<pre><code class="language-powershell">caddy fmt --overwrite Caddyfile
</code></pre>
<hr>
<h2 id="🔁-10-troubleshooting-cheat-sheet">🔁 10. Troubleshooting Cheat Sheet</h2>
<table>
<thead>
<tr>
<th>Problem</th>
<th>Why it happens</th>
<th>Quick fix</th>
</tr>
</thead>
<tbody><tr>
<td>Access denied on port 80/443</td>
<td>IIS or another listener is running</td>
<td>Stop IIS with <code>Stop-Service W3SVC -Force</code></td>
</tr>
<tr>
<td>Browser still warns “Not secure”</td>
<td>Trust store missing mkcert root</td>
<td>Re-run <code>mkcert -install</code></td>
</tr>
<tr>
<td>Caddyfile warnings</td>
<td>Formatting or syntax issues</td>
<td><code>caddy fmt --overwrite Caddyfile</code></td>
</tr>
<tr>
<td>Domain won’t resolve</td>
<td>Hosts file wasn’t saved as admin</td>
<td>Re-open Notepad as admin, save again</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔄-11-after-every-reboot">🔄 11. After Every Reboot</h2>
<ol>
<li>Stop IIS (<code>Stop-Service W3SVC -Force</code>).  </li>
<li>Start your Next.js app (<code>npm run dev</code>).  </li>
<li>Launch Caddy (<code>caddy run --config Caddyfile</code>).  </li>
<li>Visit <code>https://localdev.corp.test</code>.</li>
</ol>
<p>Repeat whenever you restart your machine or swap branches.</p>
<hr>
<h2 id="🧭-summary">🧭 Summary</h2>
<table>
<thead>
<tr>
<th>Component</th>
<th>Why it matters</th>
</tr>
</thead>
<tbody><tr>
<td>Chocolatey</td>
<td>Fast install + upgrades for Windows tooling</td>
</tr>
<tr>
<td>mkcert</td>
<td>Trusted local certificates with zero manual import steps</td>
</tr>
<tr>
<td>Caddy</td>
<td>Handles HTTPS termination and reverse proxying</td>
</tr>
<tr>
<td>Hosts file</td>
<td>Routes <code>localdev.corp.test</code> to <code>127.0.0.1</code></td>
</tr>
<tr>
<td>IIS stop</td>
<td>Keeps ports 80/443 open so Caddy can bind</td>
</tr>
</tbody></table>
<p><strong>Result:</strong> your Next.js + Sitecore JSS environment is available at <code>https://localdev.corp.test</code> with a fully trusted padlock, no surprises.</p>
<hr>
]]></content:encoded>
    </item>
    <item>
      <title>Experience Edge Caching - Advanced Strategies for Sub-Second Response Times</title>
      <link>https://alikhan.tech/blogs/2025/experience-edge-caching-sub-second-response</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/experience-edge-caching-sub-second-response</guid>
      <pubDate>Sat, 28 Jun 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[Experience Edge is Sitecore's global CDN, but caching strategy makes the difference between millisecond response times and multi-second waits. Many implementations treat caching as an afterthought, resulting in stale content, cache invalidation issues, and poor Core Web Vitals.…]]></description>
      <content:encoded><![CDATA[<p>Experience Edge is Sitecore&#39;s global CDN, but caching strategy makes the difference between millisecond response times and multi-second waits. Many implementations treat caching as an afterthought, resulting in stale content, cache invalidation issues, and poor Core Web Vitals.</p>
<p>This guide covers production-grade caching patterns that deliver sub-second responses while maintaining content freshness.</p>
<hr>
<h2 id="⚠️-the-caching-challenge">⚠️ The Caching Challenge</h2>
<p><strong>Common Problems:</strong></p>
<ol>
<li><strong>Stale Content</strong>: Users see outdated information after publishing</li>
<li><strong>Cache Invalidation</strong>: Correct invalidation is harder than it seems</li>
<li><strong>Unpredictable Performance</strong>: Sometimes fast, sometimes slow</li>
<li><strong>High Latency Spikes</strong>: Traffic causes cache misses</li>
</ol>
<p><strong>Real-World Impact:</strong></p>
<ul>
<li>First Contentful Paint (FCP): 3-5 seconds (should be less than 1.8s)</li>
<li>Largest Contentful Paint (LCP): 5-8 seconds (should be less than 2.5s)</li>
<li>Bounce rate: 30-40% (should be less than 10%)</li>
</ul>
<hr>
<h2 id="🏗️-caching-layers-architecture">🏗️ Caching Layers Architecture</h2>
<pre><code>┌────────────────────────────────────────────────────┐
│              User Browser                          │
│          (Browser Cache: 1 hour)                   │
└────────────┬─────────────────────────────────────┘
             │ Cache Miss
             ↓
┌────────────────────────────────────────────────────┐
│          CDN Edge (Akamai/Cloudflare)             │
│          (CDN Cache: 5-60 minutes)                 │
└────────────┬─────────────────────────────────────┘
             │ Cache Miss
             ↓
┌────────────────────────────────────────────────────┐
│          Experience Edge (Sitecore)               │
│          (Origin Cache: 10 minutes)               │
└────────────┬─────────────────────────────────────┘
             │ Cache Miss
             ↓
┌────────────────────────────────────────────────────┐
│          Next.js Application                       │
│          (ISR Cache + Application Logic)          │
└────────────┬─────────────────────────────────────┘
             │ Cache Miss
             ↓
┌────────────────────────────────────────────────────┐
│          Sitecore XM Cloud (GraphQL)              │
│          (Layout Service, Item Resolution)        │
└────────────────────────────────────────────────────┘
</code></pre>
<hr>
<h2 id="🌐-part-1-cdn-caching-configuration">🌐 Part 1: CDN Caching Configuration</h2>
<h3 id="setting-correct-cache-headers">Setting Correct Cache Headers</h3>
<pre><code class="language-typescript">// next.config.js
module.exports = {
  headers: async () =&gt; {
    return [
      // Static pages: Long cache
      {
        source: &#39;/static/:path*&#39;,
        headers: [
          {
            key: &#39;Cache-Control&#39;,
            value: &#39;public, s-maxage=604800, stale-while-revalidate=1209600&#39;
            // CDN: 7 days, Stale-while-revalidate: 14 days
          }
        ]
      },
      // Dynamic content: Short cache with revalidation
      {
        source: &#39;/news/:path*&#39;,
        headers: [
          {
            key: &#39;Cache-Control&#39;,
            value: &#39;public, s-maxage=300, stale-while-revalidate=600&#39;
            // CDN: 5 minutes, SWR: 10 minutes
          },
          {
            key: &#39;Vary&#39;,
            value: &#39;Accept-Encoding, User-Agent&#39;
          }
        ]
      },
      // Personalized content: No cache
      {
        source: &#39;/account/:path*&#39;,
        headers: [
          {
            key: &#39;Cache-Control&#39;,
            value: &#39;private, no-cache, no-store, must-revalidate&#39;
          }
        ]
      }
    ];
  }
};
</code></pre>
<h3 id="cache-headers-reference">Cache Headers Reference</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Purpose</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><code>s-maxage</code></td>
<td>CDN cache duration (seconds)</td>
<td><code>s-maxage=300</code> (5 min)</td>
</tr>
<tr>
<td><code>stale-while-revalidate</code></td>
<td>Serve stale while refreshing</td>
<td><code>stale-while-revalidate=600</code></td>
</tr>
<tr>
<td><code>Vary</code></td>
<td>Cache key (vary by header)</td>
<td><code>Vary: Accept-Encoding</code></td>
</tr>
<tr>
<td><code>ETag</code></td>
<td>Content version identifier</td>
<td><code>ETag: &quot;abc123&quot;</code></td>
</tr>
<tr>
<td><code>Last-Modified</code></td>
<td>Last update timestamp</td>
<td>For conditional requests</td>
</tr>
</tbody></table>
<hr>
<h2 id="⚡-part-2-nextjs-isr-configuration">⚡ Part 2: Next.js ISR Configuration</h2>
<h3 id="isr-incremental-static-regeneration">ISR (Incremental Static Regeneration)</h3>
<p>ISR combines static generation with dynamic updates.</p>
<pre><code class="language-typescript">// pages/blog/[slug].tsx
export default function BlogPost({ post }) {
  return (
    &lt;article&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
      &lt;time&gt;{post.publishedDate}&lt;/time&gt;
    &lt;/article&gt;
  );
}

export async function getStaticProps({ params }) {
  const post = await getPostFromXmCloud(params.slug);

  return {
    props: { post },
    revalidate: 60  // ISR: revalidate every 60 seconds
  };
}

export async function getStaticPaths() {
  // Generate paths for popular posts at build time
  const paths = await getPopularPosts();

  return {
    paths: paths.map(post =&gt; ({ params: { slug: post.slug } })),
    fallback: &#39;blocking&#39;  // Generate unknown paths on-demand
  };
}
</code></pre>
<h3 id="isr-per-content-type">ISR Per Content Type</h3>
<p>Different content types need different revalidation rates:</p>
<pre><code class="language-typescript">interface RevalidationConfig {
  contentType: string;
  revalidateSeconds: number;
  reason: string;
}

const revalidationConfig: RevalidationConfig[] = [
  {
    contentType: &#39;News Article&#39;,
    revalidateSeconds: 60,
    reason: &#39;Frequent updates, time-sensitive&#39;
  },
  {
    contentType: &#39;Blog Post&#39;,
    revalidateSeconds: 300,
    reason: &#39;Updated occasionally&#39;
  },
  {
    contentType: &#39;Product Details&#39;,
    revalidateSeconds: 1800,
    reason: &#39;Infrequently changed&#39;
  },
  {
    contentType: &#39;Static Page&#39;,
    revalidateSeconds: 86400,
    reason: &#39;Annual updates&#39;
  }
];

function getRevalidateTime(template: string): number {
  const config = revalidationConfig.find(c =&gt; c.contentType === template);
  return config?.revalidateSeconds ?? 300;
}
</code></pre>
<h3 id="on-demand-revalidation">On-Demand Revalidation</h3>
<p>Trigger revalidation immediately after publishing:</p>
<pre><code class="language-typescript">// pages/api/revalidate.ts
export default async function handler(req, res) {
  // Verify webhook is from Sitecore
  const signature = req.headers[&#39;x-sitecore-signature&#39;];
  if (!verifySignature(req.body, signature)) {
    return res.status(401).json({ message: &#39;Unauthorized&#39; });
  }

  const { itemId, path } = req.body;

  try {
    // Revalidate specific paths
    await res.revalidate(path);
    
    // Also revalidate related pages
    const relatedPaths = await getRelatedPages(itemId);
    for (const relatedPath of relatedPaths) {
      await res.revalidate(relatedPath);
    }

    return res.json({
      revalidated: true,
      paths: [path, ...relatedPaths]
    });
  } catch (error) {
    console.error(&#39;Revalidation failed:&#39;, error);
    return res.status(500).json({ message: &#39;Revalidation failed&#39; });
  }
}

// Sitecore webhook configuration
// POST /api/revalidate
// Trigger on: Publish Complete
// Include: Item path, template name
</code></pre>
<hr>
<h2 id="🔍-part-3-graphql-query-caching">🔍 Part 3: GraphQL Query Caching</h2>
<h3 id="query-level-cache-configuration">Query-Level Cache Configuration</h3>
<pre><code class="language-typescript">// Apollo Client cache configuration
import { InMemoryCache, HttpLink } from &#39;@apollo/client&#39;;
import { useMemo } from &#39;react&#39;;

const createApolloClient = () =&gt; {
  return new ApolloClient({
    ssrMode: typeof window === &#39;undefined&#39;,
    link: new HttpLink({
      uri: &#39;/api/graphql&#39;,
      credentials: &#39;same-origin&#39;,
      headers: {
        &#39;sc-apikey&#39;: process.env.NEXT_PUBLIC_SITECORE_API_KEY
      }
    }),
    cache: new InMemoryCache({
      typePolicies: {
        Item: {
          keyFields: [&#39;id&#39;],
          fields: {
            // Cache this field for 10 minutes
            children: {
              merge(existing, incoming) {
                return incoming;
              },
              read(existing) {
                return existing;
              }
            }
          }
        },
        Query: {
          fields: {
            // Cache search results
            search: {
              merge(existing = {}, incoming) {
                return { ...existing, ...incoming };
              }
            }
          }
        }
      }
    })
  });
};
</code></pre>
<h3 id="query-complexity-optimization">Query Complexity Optimization</h3>
<pre><code class="language-graphql"># INEFFICIENT: Over-fetching nested data
query GetPageWithAllData {
  item(path: &quot;/home&quot;) {
    id
    name
    fields {
      name
      value
    }
    children {
      id
      name
      children {
        id
        name
        children {  # Too deep!
          id
          name
        }
      }
    }
  }
}

# EFFICIENT: Fetch only needed data
query GetPageWithOptimizedFields {
  item(path: &quot;/home&quot;) {
    id
    name
    title: field(name: &quot;Title&quot;) {
      value
    }
    description: field(name: &quot;Description&quot;) {
      value
    }
  }
}

# EFFICIENT: Separate queries for different concerns
fragment PageFields on Item {
  id
  name
  title: field(name: &quot;Title&quot;) { value }
}

fragment ChildrenSummary on Item {
  id
  name
  url
}

query GetPage {
  item(path: &quot;/home&quot;) {
    ...PageFields
    children(first: 10) {
      ...ChildrenSummary
    }
  }
}
</code></pre>
<hr>
<h2 id="🔄-part-4-cache-invalidation-patterns">🔄 Part 4: Cache Invalidation Patterns</h2>
<h3 id="pattern-1-time-based-invalidation">Pattern 1: Time-Based Invalidation</h3>
<pre><code class="language-typescript">// Simple: Set TTL and let cache expire
const config = {
  news: { ttl: 5 * 60 },        // 5 minutes
  blog: { ttl: 30 * 60 },       // 30 minutes
  product: { ttl: 2 * 60 * 60 } // 2 hours
};
</code></pre>
<h3 id="pattern-2-event-based-invalidation">Pattern 2: Event-Based Invalidation</h3>
<pre><code class="language-typescript">// Listen to XM Cloud publish events
app.post(&#39;/webhooks/item-published&#39;, async (req, res) =&gt; {
  const { itemId, language, version } = req.body;

  // 1. Get item details
  const item = await xmCloud.getItem(itemId, language);

  // 2. Determine affected URLs
  const urls = await getUrlsForItem(item);

  // 3. Invalidate caches
  for (const url of urls) {
    // Invalidate CDN
    await cdn.purge(url);
    
    // Trigger ISR
    await fetch(`${process.env.SITE_URL}/api/revalidate`, {
      method: &#39;POST&#39;,
      body: JSON.stringify({ path: url, itemId })
    });

    // Invalidate Apollo cache
    await invalidateApolloCache(itemId);
  }

  res.status(200).json({ invalidated: urls.length });
});
</code></pre>
<h3 id="pattern-3-smart-cache-busting">Pattern 3: Smart Cache Busting</h3>
<pre><code class="language-typescript">// Include version hash in URLs
const getAssetUrl = (path: string, version: string) =&gt; {
  return `${path}?v=${version}`;
};

// Sitecore item hash
const itemHash = createHash(&#39;sha256&#39;)
  .update(JSON.stringify(item))
  .digest(&#39;hex&#39;)
  .substring(0, 8);

// URLs include hash
const urls = {
  css: `/styles/main.css?v=${itemHash}`,
  js: `/scripts/app.js?v=${itemHash}`,
  json: `/api/item/${itemId}.json?v=${itemHash}`
};

// When item changes, hash changes, URLs change, browser fetches new version
</code></pre>
<hr>
<h2 id="🐛-part-5-debugging-stale-cache-issues">🐛 Part 5: Debugging Stale Cache Issues</h2>
<h3 id="identifying-cache-problems">Identifying Cache Problems</h3>
<pre><code class="language-typescript">// Middleware to detect cache issues
export async function middleware(req, res, next) {
  const cacheKey = req.url;
  const cachedAt = req.headers[&#39;x-cache-date&#39;];
  const now = Date.now();
  
  // Check if content is stale
  if (cachedAt &amp;&amp; (now - parseInt(cachedAt)) &gt; 300000) {
    // More than 5 minutes old
    console.warn(`Stale content served: ${cacheKey}`);
  }

  // Add diagnostic headers
  res.setHeader(&#39;X-Cache-Date&#39;, now.toString());
  res.setHeader(&#39;X-Cache-Status&#39;, &#39;HIT/MISS&#39;);
  res.setHeader(&#39;X-Cache-Age&#39;, (now - parseInt(cachedAt || 0)).toString());

  next();
}
</code></pre>
<h3 id="browser-devtools-debugging">Browser DevTools Debugging</h3>
<pre><code class="language-typescript">// Log cache status in browser console
fetch(&#39;/api/page&#39;, {
  headers: {
    &#39;Accept-Encoding&#39;: &#39;gzip&#39;,
    &#39;User-Agent&#39;: navigator.userAgent
  }
}).then(response =&gt; {
  console.log(&#39;Cache Headers:&#39;, {
    cacheControl: response.headers.get(&#39;cache-control&#39;),
    etag: response.headers.get(&#39;etag&#39;),
    lastModified: response.headers.get(&#39;last-modified&#39;),
    age: response.headers.get(&#39;age&#39;),  // CDN age
    via: response.headers.get(&#39;via&#39;)   // CDN provider
  });
});
</code></pre>
<h3 id="troubleshooting-matrix">Troubleshooting Matrix</h3>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Cause</th>
<th>Solution</th>
</tr>
</thead>
<tbody><tr>
<td>Content always fresh but slow</td>
<td>Cache disabled</td>
<td>Check Cache-Control headers</td>
</tr>
<tr>
<td>Stale content shown</td>
<td>TTL too long</td>
<td>Reduce <code>s-maxage</code>, set revalidate</td>
</tr>
<tr>
<td>&quot;Always different&quot; responses</td>
<td>Cache key issues</td>
<td>Check <code>Vary</code> headers</td>
</tr>
<tr>
<td>High latency spikes</td>
<td>Cache miss storms</td>
<td>Enable stale-while-revalidate</td>
</tr>
<tr>
<td>Users see different content</td>
<td>Vary header missing</td>
<td>Add <code>Vary: Accept-Encoding</code></td>
</tr>
</tbody></table>
<h2 id="🎯-conclusion">🎯 Conclusion</h2>
<p>Sub-second response times require coordinated caching across multiple layers. Start with ISR and CDN cache headers, then add sophisticated invalidation as needed. Monitor continuously and adjust TTLs based on real user data.</p>
<hr>
]]></content:encoded>
    </item>
    <item>
      <title>From Excel to Sitecore - My Streamlined Migration Workflow via PowerShell</title>
      <link>https://alikhan.tech/blogs/2025/content-migration-the-smart-way</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/content-migration-the-smart-way</guid>
      <pubDate>Sat, 14 Jun 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[Content migration projects are notoriously painful Excel files flying around email threads, editors losing track of versions, and developers drowning in manual imports. I recently implemented a workflow that keeps Excel in Sitecore Media Library, transforms it to JSON with…]]></description>
      <content:encoded><![CDATA[<p>Content migration projects are notoriously painful Excel files flying around email threads, editors losing track of versions, and developers drowning in manual imports. I recently implemented a workflow that <strong>keeps Excel in Sitecore Media Library, transforms it to JSON with Next.js, and imports directly into Sitecore via PowerShell</strong>. No temp files, no duplication, just clean automation.</p>
<h2 id="the-challenge">The Challenge</h2>
<ul>
<li>Content teams live in <strong>Excel</strong>.</li>
<li>Developers prefer structured formats like <strong>JSON</strong>.</li>
<li>Sitecore needs clean, validated data to create content items at scale.</li>
</ul>
<p>Traditional approaches meant exporting JSON files, juggling temp folders, and scripting one-off transformations. I wanted a system that was <strong>repeatable, auditable, and fully API-driven</strong>.</p>
<hr>
<h2 id="the-architecture">The Architecture</h2>
<ol>
<li><strong>Excel in Sitecore Media Library</strong>  → Editors upload workbooks as media items.</li>
<li><strong>GraphQL Query</strong>  → Resolve the media item URL via Sitecore GraphQL.</li>
<li><strong>Next.js API (with <code>xlsx</code>)</strong>  → Download, parse, normalize headers, return JSON.</li>
<li><strong>PowerShell + SPE</strong>  → Call API, validate, and upsert items into Sitecore.</li>
</ol>
<p>This creates a seamless <strong>Excel → JSON → Sitecore</strong> pipeline.</p>
<hr>
<h2 id="step-1--graphql-to-resolve-the-media-item">Step 1 — GraphQL to Resolve the Media Item</h2>
<p>We start by resolving the Excel file stored in the Media Library:</p>
<pre><code class="language-graphql">query ExcelMedia($id: String!) {
  item(path: $id, language: &quot;en&quot;) {
    ... on MediaItem {
      id
      name
      extension
      url: mediaUrl
    }
  }
}
</code></pre>
<p>This gives us the download URL for the Excel file.</p>
<hr>
<h2 id="step-2--nextjs-api-parse-excel-into-json">Step 2 — Next.js API: Parse Excel into JSON</h2>
<p>The API fetches the media binary, parses it with <code>xlsx</code>, and maps it to clean JSON. Features include:</p>
<ul>
<li><strong>Column mapping dictionary</strong> (<code>Page Title</code> → <code>Title</code>)</li>
<li><strong>Validation</strong> (required fields)</li>
<li><strong>Duplicate detection</strong> (ExternalId or Title)</li>
<li><strong>Dry-run mode</strong></li>
<li><strong>Sheet selection</strong> (<code>?sheet=LandingPages</code>)</li>
</ul>
<pre><code class="language-ts">import * as XLSX from &quot;xlsx&quot;;

const COLUMN_MAP = {
  &quot;title&quot;: &quot;Title&quot;,
  &quot;page title&quot;: &quot;Title&quot;,
  &quot;summary&quot;: &quot;Summary&quot;,
  &quot;body&quot;: &quot;Body&quot;,
  &quot;content&quot;: &quot;Body&quot;,
  &quot;category&quot;: &quot;Category&quot;,
  &quot;external id&quot;: &quot;ExternalId&quot;,
  &quot;id&quot;: &quot;ExternalId&quot;
};

// Normalize + validate rows
type MappedRow = { Title: string; Summary?: string; Body?: string; Category?: string; ExternalId?: string };

function mapRow(raw: Record&lt;string, unknown&gt;): MappedRow {
  const mapped: Partial&lt;MappedRow&gt; = {};
  for (const [k, v] of Object.entries(raw)) {
    const key = String(k).toLowerCase().trim();
    const field = COLUMN_MAP[key];
    if (!field) continue;
    mapped[field] = String(v ?? &quot;&quot;);
  }
  mapped.Title = (mapped.Title ?? &quot;Untitled&quot;).trim();
  mapped.Category = mapped.Category ?? &quot;General&quot;;
  return mapped as MappedRow;
}

export async function GET(req: Request) {
  const url = new URL(req.url);
  const mediaId = url.searchParams.get(&quot;mediaId&quot;);
  const dryRun = url.searchParams.get(&quot;dryRun&quot;) === &quot;true&quot;;

  // 1. Resolve Media URL via GraphQL (omitted here for brevity)
  // 2. Fetch binary
  // 3. Parse workbook

  const wb = XLSX.read(/* arrayBuffer */, { type: &quot;array&quot; });
  const ws = wb.Sheets[wb.SheetNames[0]];
  const raw = XLSX.utils.sheet_to_json(ws, { defval: &quot;&quot; });

  const mapped = raw.map(mapRow);

  return Response.json({
    dryRun,
    count: mapped.length,
    data: mapped
  });
}
</code></pre>
<hr>
<h2 id="step-3--powershell-importer">Step 3 — PowerShell Importer</h2>
<p>The PowerShell script calls the API, validates rows, and upserts into Sitecore. It respects <strong>dry-run mode</strong> and reports <strong>created, updated, skipped, failed</strong>.</p>
<pre><code class="language-powershell">param(
  [string]$MediaId = &quot;media:/sitecore/media library/Your/Path/content.xlsx&quot;,
  [string]$ApiBase = &quot;https://yourapp.com/api/import/parse-excel&quot;,
  [switch]$DryRun
)

$endpoint = &quot;$ApiBase?mediaId=$([uri]::EscapeDataString($MediaId))&quot; + ($(if ($DryRun) { &quot;&amp;dryRun=true&quot; } else { &quot;&quot; }))
$response = Invoke-RestMethod -Uri $endpoint -Method Get

$parentPath = &quot;master:/sitecore/content/YourSite/Migrated&quot;
$templateId = &quot;{TEMPLATE-GUID-HERE}&quot;

foreach ($row in $response.data) {
  $title = $row.Title
  $name  = [Sitecore.Data.Items.ItemUtil]::ProposeValidItemName($title)

  # Resolve existing by ExternalId or Title
  $item = $null
  if ($row.ExternalId) { $item = Get-Item &quot;$parentPath/*&quot; | Where-Object { $_[&quot;ExternalId&quot;] -eq $row.ExternalId } | Select-Object -First 1 }
  if (-not $item) { $item = Get-Item &quot;$parentPath/*&quot; | Where-Object { $_[&quot;Title&quot;] -eq $title } | Select-Object -First 1 }

  if (-not $item) {
    $item = New-Item -Path $parentPath -Name $name -ItemType $templateId
  }

  $item.Editing.BeginEdit()
  try {
    $item[&quot;Title&quot;]      = $row.Title
    $item[&quot;Summary&quot;]    = $row.Summary
    $item[&quot;Body&quot;]       = $row.Body
    $item[&quot;Category&quot;]   = $row.Category
    $item[&quot;ExternalId&quot;] = $row.ExternalId
  } finally {
    $item.Editing.EndEdit()
  }
}
</code></pre>
<hr>
<h2 id="hardening--enhancements">Hardening &amp; Enhancements</h2>
<ul>
<li><strong>Dry-run mode</strong> → editors see validation errors without creating items.</li>
<li><strong>Row-level error reporting</strong> → API returns <code>{ rowNumber, messages }</code> for invalid rows.</li>
<li><strong>Duplicate detection</strong> → prevents double imports by checking <code>ExternalId</code>/<code>Title</code>.</li>
<li><strong>Observability</strong> → counts, logs, and metrics pushed to dashboards.</li>
<li><strong>Security</strong> → protect API with signed tokens or IP allowlisting.</li>
</ul>
<hr>
<h2 id="why-this-works">Why This Works</h2>
<p>✅ Editors stay in their comfort zone (Excel in Media Library).</p>
<p>✅ Developers get a stable JSON contract.</p>
<p>✅ PowerShell focuses on item creation, not messy parsing.</p>
<p>✅ Re-runs are safe thanks to upserts + duplicate detection.</p>
<p>This isn’t just about moving content—it’s about building a <strong>reliable migration engine</strong> that’s reusable for future projects.</p>
<hr>
<h2 id="final-thought">Final Thought</h2>
<p>When migrations are API-driven, versioned, and validated at the edge, you get <strong>repeatability, safety, and speed</strong>. That’s exactly what this Next.js + PowerShell pipeline delivered.</p>
<hr>
]]></content:encoded>
    </item>
    <item>
      <title>Next.js Image Wrapper for Sitecore SVGs</title>
      <link>https://alikhan.tech/blogs/2025/nextjs-image-wrapper</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/nextjs-image-wrapper</guid>
      <pubDate>Sat, 10 May 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[Our team is consistently hitting a wall with next/image component. When a designer hands off a vector graphic, it gets flattened into a static img tag, completely stripping it of its inherent scalability and styling benefits. This isn't just a minor issue; it's a performance…]]></description>
      <content:encoded><![CDATA[<p>Our team is consistently hitting a wall with next/image component. When a designer hands off a vector graphic, it gets flattened into a static img tag, completely stripping it of its inherent scalability and styling benefits. This isn&#39;t just a minor issue; it&#39;s a performance bottleneck, a hit to our dynamic styling capabilities, and a constant source of technical debt.</p>
<p>This post will walk through a robust, production-ready solution: a custom Next.js Image Wrapper that not only gracefully handles all media from Sitecore but also intelligently renders SVGs as true inline svg markup.</p>
<h2 id="executive-summary">Executive Summary</h2>
<ul>
<li><strong>The Problem:</strong> The default <code>next/image</code> component treats SVGs as raster images, limiting styling and DOM manipulation. We also need a bulletproof way to handle missing <code>width</code>, <code>height</code>, and <code>alt</code> attributes from Sitecore.</li>
<li><strong>Inline SVG vs. <code>next/image</code>:</strong> By rendering SVGs as inline <code>&lt;svg&gt;</code> markup, we gain full CSS control (e.g., <code>currentColor</code> for theming), enable accessibility hooks, and simplify DOM manipulation. For all other image types, we leverage the power of <code>next/image</code> for automatic optimization.</li>
<li><strong>Bulletproof Fallbacks:</strong> Our wrapper component provides sensible defaults for <code>width</code> and <code>height</code> if they are missing from the Sitecore Media Library, preventing Cumulative Layout Shift (CLS) and ensuring consistent layouts.</li>
<li><strong>Key Outcomes:</strong> This approach leads to superior performance (no extra network requests for styling), enhanced accessibility, and a more maintainable, consistent frontend architecture. It’s about building a predictable and resilient component system.</li>
</ul>
<hr>
<h2 id="the-problem">The Problem</h2>
<p>Sitecore’s Media Library is a fantastic tool for managing digital assets. It provides URLs for images, but when it comes to SVGs, a URL is often not enough. A standard <code>&lt;img&gt;</code> tag or <code>next/image</code> component treats the SVG file just like a PNG or JPG. We lose:</p>
<ul>
<li><strong>Styling with CSS:</strong> Can’t use <code>fill</code>, <code>stroke</code>, or <code>color</code> to change the icon&#39;s appearance based on state or theme. This is critical for design systems and Dark Mode.</li>
<li><strong>DOM Interaction:</strong> No way to programmatically manipulate the SVG&#39;s internal paths or elements with JavaScript.</li>
<li><strong>Accessibility:</strong> The <code>alt</code> text is on the wrapper <code>&lt;img&gt;</code> tag, but you can’t add rich accessibility attributes like <code>aria-labelledby</code> or <code>role=&quot;img&quot;</code> directly to the <code>&lt;svg&gt;</code> itself.</li>
<li><strong>Performance:</strong> The browser makes an extra network request to download the SVG, even if it&#39;s a tiny icon, instead of having the markup directly in the DOM.</li>
</ul>
<p>On top of this, CMS authors often forget or don&#39;t know to fill in the <code>width</code>, <code>height</code>, or <code>alt</code> fields for an image, leading to layout shifts and accessibility violations. We need a way to enforce consistency without manual intervention.</p>
<hr>
<h2 id="the-solution-architecture-overview">The Solution: Architecture Overview</h2>
<p>Our solution is an intelligent wrapper component that sits in front of the standard <code>next/image</code>. Its primary job is to inspect the <code>src</code> URL and make a critical decision: should this be a flexible, inline <code>&lt;svg&gt;</code> or a performant, optimized <code>&lt;Image&gt;</code>?</p>
<p>Here’s the decision flow:</p>
<pre><code>props (field, width, height)
         └─&gt; isSVG? (check file extension)
         |   └─&gt; is in Experience Editor or Preview mode? -&gt; &lt;NextImage /&gt;
         |   └─&gt; else -&gt; fetch SVG content on the server -&gt; render &lt;SVGInline /&gt;
         |
         └─&gt; else (e.g., .jpg, .png)
             └─&gt; has all dimensions? -&gt; &lt;NextImage /&gt;
             └─&gt; else (missing dimensions)
                 └─&gt; is in Experience Editor? -&gt; render red warning + &lt;NextImage /&gt;
                 └─&gt; else -&gt; render empty tag to prevent layout shift
</code></pre>
<p>This architecture ensures we get the best of both worlds: the power of inline SVGs where we need it and the performance benefits of <code>next/image</code> for everything else. The fallback logic for missing dimensions is a critical safety net that prevents layout shifts (CLS) and provides clear feedback to content authors in the editor.</p>
<hr>
<h2 id="code-walkthrough">Code Walkthrough</h2>
<p>Let&#39;s dissect the code files to see how this all comes together.</p>
<h3 id="nextimageextendedtsx"><code>NextImageExtended.tsx</code></h3>
<p>This is our main wrapper component. It&#39;s the &quot;brain&quot; that makes all the decisions.</p>
<p><strong>File Name &amp; Role:</strong> <code>NextImageExtended.tsx</code>. This component serves as a drop-in replacement for the default Sitecore JSS <code>NextImage</code> component. Its primary responsibility is to orchestrate the rendering logic based on the image&#39;s properties.</p>
<p><strong>Key Exports:</strong></p>
<ul>
<li><code>NextImageExtended</code>: The core component that handles the rendering logic.</li>
<li><code>NextImageProps</code>: A TypeScript interface to define the expected props, ensuring type safety.</li>
</ul>
<p><strong>Code:</strong></p>
<pre><code class="language-tsx">import useIsEditing from &#39;@/hooks/useIsEditing&#39;;
import {
  ImageField,
  LayoutServicePageState,
  NextImage,
  useSitecoreContext,
} from &#39;@sitecore-jss/sitecore-jss-nextjs&#39;;
import SVGInline from &#39;./SVGInline&#39;;

type NextImageProps {
  field?: ImageField;
  width?: number;
  height?: number;
}

const NextImageExtended = (props: NextImageProps): JSX.Element =&gt; {
  const { sitecoreContext } = useSitecoreContext();
  const isEditing = useIsEditing();
  const isPreview = sitecoreContext.pageState == LayoutServicePageState.Preview;

  if (!!props.width &amp;&amp; props.field?.value) {
    props.field.value.width = props.width;
  }
  if (!!props.height &amp;&amp; props.field?.value) {
    props.field.value.height = props.height;
  }
  const hasEmptyDimension =
    props?.field?.value?.src !== undefined &amp;&amp;
    (props.field?.value?.width === undefined ||
      props.field?.value?.width === &#39;&#39; ||
      props.field?.value?.height === undefined ||
      props.field?.value?.height == &#39;&#39;);

  if (props.field?.value?.src?.includes(&#39;.svg&#39;) &amp;&amp; !(isEditing || isPreview)) {
    let src = props.field?.value?.src;
    src = &#39;/api/fetchSvg?url=${encodeURIComponent(src)}&#39;;
    return &lt;SVGInline src={src} /&gt;;
  } else if (props.field?.value?.src?.includes(&#39;.svg&#39;) &amp;&amp; hasEmptyDimension) {
    props.field.value.width = 50;
    props.field.value.height = 50;

    return &lt;NextImage {...props} /&gt;;
  } else if (hasEmptyDimension) {
    if (isEditing) {
      return (
        &lt;&gt;
          &lt;span style={{ color: &#39;red&#39; }}&gt;Fill in image dimensions&lt;/span&gt;
          &lt;NextImage {...props} /&gt;
        &lt;/&gt;
      );
    } else {
      return &lt;&gt;&lt;/&gt;;
    }
  } else {
    return &lt;NextImage {...props} /&gt;;
  }
};

export default NextImageExtended;
</code></pre>
<p><strong>Why this matters:</strong></p>
<ul>
<li><p><strong>Experience Editor Exemption:</strong> We intentionally <em>don&#39;t</em> render inline SVGs in the Experience Editor. This is a crucial design choice. The editor often requires a specific <code>&lt;img&gt;</code> tag structure to enable its rich-text and image editing capabilities. The <code>useIsEditing()</code> and <code>useSitecoreContext()</code> hooks are the perfect way to make this distinction, ensuring the authoring experience is not broken.</p>
</li>
<li><p><strong>Server-Side Fetching:</strong> The <code>src</code> is rewritten to <code>/api/fetchSvg</code>. This is a powerful move. By fetching the SVG content on the server side (via a Next.js API route), we can get the raw markup <em>before</em> the component is rendered. This is essential for server-side rendering (SSR) and Incremental Static Regeneration (ISR). The <code>&lt;SVGInline&gt;</code> component then renders this already-fetched and sanitized markup. This avoids a client-side fetch, improving performance.</p>
</li>
<li><p><strong>Handling Missing Dimensions:</strong> The <code>hasEmptyDimension</code> check is our fallback logic.</p>
</li>
<li><p><strong>Preventing CLS:</strong> Setting default dimensions prevents a massive layout shift. <code>next/image</code> requires these values to calculate the aspect ratio, which is key to preventing CLS.</p>
</li>
<li><p><strong>Content Author Feedback:</strong> In editing mode, we show a red warning. This isn&#39;t just a band-aid; it&#39;s a user experience improvement for the content author, teaching them that dimensions are important.</p>
</li>
<li><p><strong>Graceful Degradation:</strong> In production, if dimensions are still missing, we just render an empty <code>&lt;&gt;&lt;/&gt;</code> fragment. This is an opinionated choice: better to render nothing than to cause a massive layout shift or a broken layout.</p>
</li>
</ul>
<h3 id="fetchsvgts"><code>fetchSvg.ts</code></h3>
<p>This is our Next.js API route. It&#39;s a simple, single-purpose utility.</p>
<p><strong>File Name &amp; Role:</strong> <code>fetchSvg.ts</code>. This file defines a <strong>serverless function</strong> that lives in the <code>/pages/api</code> directory (or <code>/app/api</code> in Next.js 13+). Its sole purpose is to securely fetch the raw content of a given SVG URL.</p>
<p><strong>Key Exports:</strong></p>
<ul>
<li><code>handler</code>: The default export that serves as the API route.</li>
</ul>
<p><strong>Critical Code Paths:</strong></p>
<pre><code class="language-tsx">import { NextApiRequest, NextApiResponse } from &#39;next&#39;;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { url } = req.query;

  if (!url) {
    res.status(400).json({ error: &#39;Missing URL parameter&#39; });
    return;
  }

  try {
    const response = await fetch(url as string);

    if (!response.ok) {
      res.status(response.status).json({ error: &#39;Failed to fetch SVG&#39; });
      return;
    }

    const data = await response.text();
    res.setHeader(&#39;Content-Type&#39;, &#39;image/svg+xml&#39;);
    res.status(200).send(data);
  } catch (error) {
    res.status(500).json({ error: &#39;Internal Server Error&#39; });
  }
}
</code></pre>
<p><strong>Why this matters:</strong></p>
<ul>
<li><strong>Security First:</strong> By using an API route as a proxy, we can sanitize the SVG content before it reaches the client. While this code doesn&#39;t show it, a real-world implementation should use a library like <code>dompurify</code> on the <code>data</code> to strip out any malicious <code>&lt;script&gt;</code> tags or <code>on*</code> attributes. <strong>Never</strong> render raw SVG markup from an untrusted source.</li>
<li><strong>Caching and Optimization:</strong> This API route can be cached by a CDN or Next.js, meaning the SVG content is only fetched from Sitecore once. This is a huge performance win for repeat visits.</li>
<li><strong>Cross-Origin Headers:</strong> This route handles any potential cross-origin (CORS) issues between your Next.js application and the Sitecore Media Library, as it acts as a server-side intermediary.</li>
</ul>
<hr>
<h3 id="migration-checklist">Migration Checklist</h3>
<p>Ready to adopt this into your project? Here&#39;s a quick checklist:</p>
<ol>
<li><strong>Create the files:</strong> Add <code>NextImageExtended.tsx</code>, <code>SVGInline.tsx</code> (a simple component that takes <code>src</code> as props and renders the inner HTML), and the <code>fetchSvg.ts</code> API route.</li>
<li><strong>Add Types &amp; Helpers:</strong> Ensure you have the necessary types and the <code>useIsEditing</code> hook.</li>
<li><strong>Update your components:</strong> Find and replace all instances of <code>&lt;NextImage ... /&gt;</code> with <code>&lt;NextImageExtended ... /&gt;</code>.</li>
<li><strong>Sanitize:</strong> Integrate a sanitation library like <code>dompurify</code> into the <code>fetchSvg.ts</code> route for security.</li>
<li><strong>Test:</strong> Verify that images and SVGs render correctly in both production and Experience Editor modes. Ensure missing dimensions are handled gracefully.</li>
</ol>
<p>By following this approach, you can deliver a more performant, accessible, and maintainable frontend experience for your Sitecore solution. It’s about being deliberate with our design choices and creating a component system that is both intelligent and resilient.</p>
]]></content:encoded>
    </item>
    <item>
      <title>AI-Powered Content Creation in XM Cloud with Sitecore Stream</title>
      <link>https://alikhan.tech/blogs/2025/xm-cloud-with-sitecore-stream</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/xm-cloud-with-sitecore-stream</guid>
      <pubDate>Sun, 16 Mar 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[Sitecore recently released Sitecore Stream, a powerful addition to its expanding suite of digital experience solutions. First introduced at last October’s Sitecore Symposium, this new tool is designed to help marketing teams create, manage, and optimize content more efficiently…]]></description>
      <content:encoded><![CDATA[<p>Sitecore recently released Sitecore Stream, a powerful addition to its expanding suite of digital experience solutions. First introduced at last October’s Sitecore Symposium, this new tool is designed to help marketing teams create, manage, and optimize content more efficiently while ensuring brand consistency. As organizations scale their content production and personalization strategies, the demand for solutions that simplify workflows and uphold brand integrity continues to grow.</p>
<p>Let’s dive into how this integration is changing the game for digital marketers.</p>
<h2 id="why-ai-content-optimization-in-xm-cloud-is-a-game-changer">Why AI Content Optimization in XM Cloud is a Game-Changer</h2>
<p>Creating high-performing, on-brand content at scale has always been a challenge. With Sitecore Stream’s AI integration in XM Cloud, marketers now have:</p>
<ul>
<li><strong>Instant content generation</strong> – AI drafts text for headlines, paragraphs, and CTAs in seconds.</li>
<li><strong>Real-time optimization</strong> – Fix grammar, adjust tone, shorten or expand text with a click.</li>
<li><strong>Brand-aware AI</strong> – Ensures all content aligns with your brand guidelines (for Stream Premium users).</li>
<li><strong>Data-driven decisions</strong> – A/B test AI-optimized content to determine the best-performing version.</li>
</ul>
<p>No more staring at a blank page—just <strong>faster, smarter content creation.</strong></p>
<h2 id="how-ai-content-optimization-works-in-xm-cloud-pages">How AI Content Optimization Works in XM Cloud Pages</h2>
<p><img src="https://alikhan.tech/images/post/AItranslate.png" alt="image"></p>
<h3 id="generate--refine-content-in-seconds">Generate &amp; Refine Content in Seconds</h3>
<p>Inside <strong>XM Cloud&#39;s Page Editor</strong>, simply:</p>
<ol>
<li><strong>Select any text component</strong> (headline, paragraph, CTA)</li>
<li>Click <strong>&quot;Optimize Content&quot;</strong> in the right-hand panel</li>
<li>Choose an action:<ul>
<li><strong>Improve writing</strong> (sharpens clarity &amp; engagement)</li>
<li><strong>Fix spelling &amp; grammar</strong> (instant proofreading)</li>
<li><strong>Make shorter / longer</strong> (condenses or expands text)</li>
<li><strong>Change tone</strong> (adjusts voice for different audiences)</li>
<li><strong>Translate</strong> (supports multiple languages)</li>
<li><strong>Adhere to brand</strong> (applies brand guidelines if a Brand Kit is linked)</li>
</ul>
</li>
</ol>
<h3 id="ab-test-ai-optimized-content">A/B Test AI-Optimized Content</h3>
<p>Once you&#39;ve generated an optimized version, you can:</p>
<p><strong>Replace the original text</strong> immediately<br><strong>Run an A/B/n test</strong> to see which version performs better  </p>
<p><em>Example:</em>  </p>
<ul>
<li>Original: <em>&quot;Discover our latest product features.&quot;</em>  </li>
<li>AI-Optimized: <em>&quot;Unlock the full potential of our newest innovations.&quot;</em>  </li>
<li><strong>Test both variants</strong> to see which drives more engagement</li>
</ul>
<h3 id="personalize-content-with-ai">Personalize Content with AI</h3>
<p>In <strong>Personalize mode</strong>, AI helps tailor content for different audience segments. Need a more formal tone for B2B visitors and a friendly one for B2C <strong>Stream adjusts dynamically.</strong></p>
<h2 id="getting-started-with-ai-optimization-in-xm-cloud">Getting Started with AI Optimization in XM Cloud</h2>
<h3 id="requirements">Requirements:</h3>
<ul>
<li><strong>Sitecore Stream Premium</strong> (for AI content optimization)  </li>
<li><strong>New CKEditor rich text editor</strong> enabled in XM Cloud  </li>
<li><strong>JSS v22.1+</strong> (for A/B/n testing)</li>
</ul>
<h3 id="next-steps">Next Steps:</h3>
<ol>
<li><strong>Enable Stream in XM Cloud</strong> (contact Sitecore if not yet activated)  </li>
<li><strong>Link a Brand Kit</strong> (for brand-aware optimizations)  </li>
<li><strong>Start optimizing!</strong> Select any text component and let AI enhance it</li>
</ol>
<h2 id="the-future-of-ai-in-sitecore">The Future of AI in Sitecore</h2>
<p>This is just the beginning. Expect even deeper AI integrations, such as:</p>
<ul>
<li><strong>Automated personalization</strong> (AI suggesting dynamic content variations)  </li>
<li><strong>Predictive content scoring</strong> (AI forecasting engagement before publishing)  </li>
<li><strong>Voice &amp; visual brand alignment</strong> (AI ensuring multimedia stays on-brand)</li>
</ul>
<p><strong>Note:</strong> The points about future AI integrations in Sitecore are educated predictions based on Current Sitecore Product Direction and Sitecore&#39;s Existing AI Capabilities</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p><strong>Sitecore Stream&#39;s AI-powered content optimization in XM Cloud</strong> isn&#39;t just a productivity boost—it&#39;s a <strong>fundamental shift in how marketers and content creators create and optimize content</strong>. By combining <strong>AI speed</strong>, <strong>brand governance</strong>, and <strong>data-driven testing</strong>, Sitecore is helping teams <strong>work smarter, move faster, and drive better results</strong>.</p>
<p><strong>Ready to transform your content workflow?</strong><br>👉 <em>Reach out to Sitecore or your implementation partner to enable Stream in XM Cloud today.</em> </p>
]]></content:encoded>
    </item>
    <item>
      <title>Embracing Sitecore&apos;s Front-End Development (FED) First Approach</title>
      <link>https://alikhan.tech/blogs/2025/sitecore-fed-first-approach</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/sitecore-fed-first-approach</guid>
      <pubDate>Wed, 12 Feb 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[Sitecore's transition to the Front-End Development (FED) First approach represents a significant shift in developing with XM Cloud. This method emphasizes running the head application locally while connecting directly to the XM Cloud Preview Endpoint, streamlining the…]]></description>
      <content:encoded><![CDATA[<p>Sitecore&#39;s transition to the Front-End Development (FED) First approach represents a significant shift in developing with XM Cloud. This method emphasizes running the head application locally while connecting directly to the XM Cloud Preview Endpoint, streamlining the development process.</p>
<h2 id="challenges-with-docker-based-development">Challenges with Docker-Based Development</h2>
<p>Initially, Sitecore developers utilized Docker-based development, which involved simulating XM Cloud&#39;s backend services locally. However, this approach presented several challenges:</p>
<ul>
<li><p><strong>Incomplete Service Simulation</strong>: Not all XM Cloud services could be effectively replicated locally, limiting access to features like Pages, Forms, and Components. </p>
</li>
<li><p><strong>Platform Limitations</strong>: The reliance on .NET Framework necessitated Windows-based containers, restricting development to Windows machines and excluding macOS or Linux users. </p>
</li>
<li><p><strong>Resource Intensiveness</strong>: Running multiple containers demanded high-spec machines, leading to increased hardware requirements and potential performance issues. </p>
</li>
<li><p><strong>Slow Startup Times</strong>: Developers often faced prolonged startup durations as numerous containers initialized, hindering productivity. </p>
</li>
<li><p><strong>Limited Offline Development</strong>: An internet connection remained essential for authentication and content management tasks, even when working locally.</p>
</li>
</ul>
<h2 id="advantages-of-the-fed-first-approach">Advantages of the FED First Approach</h2>
<p>The FED First approach addresses these challenges by allowing developers to run only the head application locally, connecting directly to the XM Cloud Preview Endpoint. This offers several benefits</p>
<ul>
<li><p><strong>Full Access to Services</strong>: Developers can utilize all XM Cloud backend applications, including Pages, Forms, and Components, without local simulation. </p>
</li>
<li><p><strong>Cross-Platform Compatibility</strong>: By eliminating the need for Windows-specific containers, development becomes feasible on Windows, macOS, and Linux systems. </p>
</li>
<li><p><strong>Reduced Resource Requirements</strong>: With fewer applications running locally, developers can operate effectively on machines with lower specifications. </p>
</li>
<li><p><strong>Faster Startup Times</strong>: Launching the development environment is quicker, as it involves starting only the head application. </p>
</li>
<li><p><strong>Simplified Debugging</strong>: Connecting the local head application to production environments facilitates easier debugging with live data.</p>
</li>
</ul>
<h2 id="considerations-with-the-fed-first-approach">Considerations with the FED First Approach</h2>
<p>While the FED First approach offers numerous advantages, it&#39;s important to acknowledge potential drawbacks</p>
<ul>
<li><p><strong>Dependence on Internet Connectivity</strong>: Continuous access to the XM Cloud Preview Endpoint requires a stable internet connection, which may be a limitation in environments with unreliable connectivity. </p>
</li>
<li><p><strong>Integration Challenges</strong>: Coordinating with existing systems and processes might necessitate additional configuration and testing to ensure compatibility. </p>
</li>
<li><p><strong>Limited Backend Debugging:</strong> While front-end debugging is simplified, debugging issues originating in the XM Cloud backend requires relying on logs and potentially remote debugging tools provided by Sitecore.</p>
</li>
</ul>
<ul>
<li><strong>Initial Data Setup:</strong> Ensuring the XM Cloud environment has the necessary content and configurations for your front-end development might require additional effort upfront.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Sitecore&#39;s FED First approach streamlines the development process by addressing many of the limitations inherent in Docker-based development. By enabling direct connections to XM Cloud services and supporting cross-platform development, it offers a more efficient and flexible workflow. However, developers should consider factors such as internet dependency and potential integration challenges when adopting this approach.</p>
<p>For a practical demonstration of the FED First approach, refer to the following video:</p>
<p><a href="https://www.youtube.com/watch?v=hXPBtMHQMZw">Front-End First Development in XM Cloud - YouTube</a></p>
<p><em>Note: The insights presented are based on available information and may evolve with ongoing developments in Sitecore&#39;s methodologies.</em> </p>
]]></content:encoded>
    </item>
    <item>
      <title>Workflow Email in Sitecore XM Cloud Using Webhook Submit Actions</title>
      <link>https://alikhan.tech/blogs/2025/workflow-email-in-sitecore-xm-cloud</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2025/workflow-email-in-sitecore-xm-cloud</guid>
      <pubDate>Fri, 10 Jan 2025 03:00:00 GMT</pubDate>
      <description><![CDATA[When working with workflows in Sitecore, clients often request automated notifications to keep track of important state changes. The most common requirement is email alerts. The Challenge Sitecore XM Cloud doesn't include out-of-the-box email notifications for workflow state…]]></description>
      <content:encoded><![CDATA[<p>When working with workflows in Sitecore, clients often request automated notifications to keep track of important state changes. The most common requirement is email alerts.</p>
<h3 id="the-challenge">The Challenge</h3>
<p>Sitecore XM Cloud doesn&#39;t include out-of-the-box email notifications for workflow state changes. Since XM Cloud discourages server-side customizations, we need a cloud-native solution.</p>
<h3 id="the-solution-webhook-submit-actions">The Solution: Webhook Submit Actions</h3>
<p>Webhook Submit Actions allow you to trigger HTTP calls when workflow actions occur—without writing server-side code. Here&#39;s how to set up email notifications when an item reaches the Approved state.</p>
<hr>
<h4 id="step-1-add-a-webhook-submit-action">Step 1: Add a Webhook Submit Action</h4>
<ol>
<li><p><strong>Navigate</strong> to your workflow:</p>
<ul>
<li>Go to <code>/Sitecore/System/Workflows</code> in the Content Editor</li>
<li>Open the <strong>Approved</strong> state</li>
</ul>
</li>
<li><p><strong>Insert a Webhook Submit Action</strong>:</p>
<ul>
<li>Right-click the state → <strong>Insert</strong> → <strong>Webhook Submit Action</strong></li>
</ul>
</li>
<li><p><strong>Configure the Webhook</strong>:</p>
<ul>
<li><strong>Enabled</strong>: Check to activate</li>
<li><strong>URL</strong>: Your API endpoint (e.g., Next.js/SendGrid email service)</li>
<li><strong>Serialization</strong>: Choose JSON or XML (JSON recommended)</li>
</ul>
</li>
</ol>
<h4 id="step-2-set-up-authentication-optional">Step 2: Set Up Authentication (Optional)</h4>
<p>If your API requires auth, create an <strong>Authorization Item</strong> under:
<code>/sitecore/System/Settings/Webhooks/Authorizations</code></p>
<h2 id="supported-auth-types---apikey---basic---digest">Supported auth types:
- <code>ApiKey</code>
- <code>Basic</code>
- <code>Digest</code></h2>
<h5 id="example-payload-sent-to-api">Example Payload Sent to API</h5>
<p>When the workflow triggers, Sitecore sends a JSON payload like this:</p>
<Code language="json">
{`{
  "ActionID": "d6588964-7c88-4650-a2bf-27dff4297d65",
  "ActionName": "Approval",
  "Comments": [{"Key": "Comments", "Value": "Looks perfect!"}],
  "DataItem": {
    "Id": "2e97a681-05ed-4a1c-92b0-87343b1d0e27",
    "Name": "Home",
    "TemplateId": "c6a5d6a9-85a0-4acc-8337-074270364850"
  },
  "PreviousState": {
    "DisplayName": "Draft",
    "StateID": "{A0E3A221-B774-4473-B623-1312FB5645CE}"
  },
  "UserName": "sitecore\\admin",
  "WorkflowName": "Approval Workflow"
}`}
</Code>

<h4 id="step-3-email-api-example-nextjs--sendgrid">Step 3: Email API Example (Next.js + SendGrid)</h4>
<p>Deploy this Next.js API to handle the webhook and send emails:</p>
<Code language="typescript">
{`// pages/api/workflow/notify.ts
import sgMail from '@sendgrid/mail';
import type { NextApiRequest, NextApiResponse } from 'next';

<p>// Initialize SendGrid
sgMail.setApiKey(process.env.SENDGRID_API_KEY as string);</p>
<p>interface WorkflowPayload {
  ActionID: string;
  ActionName: string;
  Comments: Array&lt;{
    Key: string;
    Value: string;
  }&gt;;
  DataItem: {
    Id: string;
    Name: string;
    TemplateId: string;
  };
  UserName: string;
  WorkflowName: string;
  PreviousState: {
    DisplayName: string;
    StateID: string;
  };
}</p>
<p>export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== &#39;POST&#39;) {
    return res.status(405).json({ message: &#39;Method not allowed&#39; });
  }</p>
<p>  try {
    const payload: WorkflowPayload = req.body;</p>
<pre><code>if (!payload.DataItem || !payload.UserName) {
  return res.status(400).json({ error: &#39;Invalid payload format&#39; });
}

const msg = {
  to: process.env.NOTIFICATION_RECIPIENT || &#39;admin@yourdomain.com&#39;,
  from: process.env.SENDER_EMAIL || &#39;noreply@yourdomain.com&#39;,
  subject: &#39;Workflow Update: &#39; + payload.DataItem.Name + &#39; Approved&#39;,
  html:
    &#39;&lt;h2&gt;Workflow Item Approved&lt;/h2&gt;&#39; +
    &#39;&lt;p&gt;&lt;strong&gt;Content Item:&lt;/strong&gt; &#39; + payload.DataItem.Name + &#39;&lt;/p&gt;&#39; +
    &#39;&lt;p&gt;&lt;strong&gt;ID:&lt;/strong&gt; &#39; + payload.DataItem.Id + &#39;&lt;/p&gt;&#39; +
    &#39;&lt;p&gt;&lt;strong&gt;Approved By:&lt;/strong&gt; &#39; + payload.UserName + &#39;&lt;/p&gt;&#39; +
    &#39;&lt;p&gt;&lt;strong&gt;Workflow:&lt;/strong&gt; &#39; +  payload.WorkflowName + &#39;&lt;/p&gt;&#39; +
    &#39;&lt;p&gt;&lt;strong&gt;Previous State:&lt;/strong&gt; &#39; + (payload.PreviousState?.DisplayName || &#39;N/A&#39;) + &#39;&lt;/p&gt;&#39; +
    &#39;&lt;p&gt;&lt;strong&gt;Comments:&lt;/strong&gt; &#39; + (payload.Comments?.[0]?.Value || &#39;No comments provided&#39;) + &#39;&lt;/p&gt;&#39; +
    &#39;&lt;p&gt;Please review the item in &lt;a href=&quot;&#39; + process.env.SITECORE_APP_URL + &#39;&quot;&gt;Sitecore&lt;/a&gt;.&lt;/p&gt;&#39;,
};

await sgMail.send(msg);

return res.status(200).json({
  success: true,
  message: &#39;Notification sent successfully&#39;,
});
</code></pre>
<p>  } catch (error) {
    console.error(&#39;Error processing workflow notification:&#39;, error);
    return res.status(500).json({
      error: &#39;Failed to process notification&#39;,
      details: error instanceof Error ? error.message : &#39;Unknown error&#39;,
    });
  }
}
`}
</Code></p>
<h3 id="final-thoughts">Final Thoughts</h3>
<p>By combining Webhook Submit Actions with a lightweight API, you can implement workflow emails in XM Cloud while adhering to best practices. Extend this further with:</p>
<ul>
<li>Notifications for rejections or draft submissions</li>
<li>Multi-channel alerts (e.g., Slack + email)</li>
</ul>
<p><strong>Pro Tip:</strong> Test your webhook with tools like webhook.site before connecting to email services.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Unlocking the Power of Geospatial in Sitecore Search</title>
      <link>https://alikhan.tech/blogs/2024/sitecore-search-understanding-geospatial</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/sitecore-search-understanding-geospatial</guid>
      <pubDate>Sat, 05 Oct 2024 03:00:00 GMT</pubDate>
      <description><![CDATA[In today’s digital world, businesses are increasingly leveraging location-based data to enhance user experiences. Whether it’s helping customers find nearby stores or delivering personalized content based on a user’s geographical location, geospatial search plays a pivotal role…]]></description>
      <content:encoded><![CDATA[<p>In today’s digital world, businesses are increasingly leveraging location-based data to enhance user experiences. Whether it’s helping customers find nearby stores or delivering personalized content based on a user’s geographical location, geospatial search plays a pivotal role in providing relevant results. Sitecore&#39;s robust search features allow for seamless integration of geospatial capabilities, making it easier than ever to implement geospatial search in your digital solutions.</p>
<h2 id="what-is-geospatial-search">What is Geospatial Search?</h2>
<p>Geospatial search refers to the ability to filter and retrieve content based on geographical data, such as latitude and longitude. This functionality is essential for industries where proximity is important, such as retail, healthcare, real estate, and logistics. Sitecore’s search engine makes it easy to filter content based on location, offering users results that are directly relevant to their current or specified geographic area.</p>
<h2 id="how-to-implement-geospatial-search-in-sitecore">How to Implement Geospatial Search in Sitecore</h2>
<p>In this section, we’ll walk through how to implement geospatial search using Sitecore&#39;s search API.</p>
<h3 id="step-1-indexing-geospatial-data">Step 1: Indexing Geospatial Data</h3>
<p>To get started, we need to create a custom field in Sitecore to store geographical coordinates. In this case, we created a field named <code>Coordinates</code> with a <code>GEO</code> data type, which can store latitude and longitude values.</p>
<p><img src="https://alikhan.tech/images/post/search-field.png" alt="image"></p>
<p>Here’s an example of indexing a record for a doctor with their location. In this case, I used Sitecore Search&#39;s PUSH source and Postman to push dummy data. The following JSON is the sample input used to feed the PUSH API via Postman:</p>
<Code language="json">
{`{
    "document": {
        "id": "1DrJohnDoe",
        "fields": {
            "name": "Dr. John Doe",
            "description": "Lorem ipsum sit a dor, lorem ipsum sit a dor, lorem ipsum",
            "type": "doctor",
            "coordinates": "40.7127837,-74.0059413"
        }
    }
}`}
</Code>

<p>This JSON document represents a doctor, with the <code>coordinates</code> field storing the latitude and longitude of their practice. This geospatial data will later be used to filter search results based on proximity.</p>
<h3 id="step-2-modifying-the-search-api-for-geo-filtering">Step 2: Modifying the Search API for Geo Filtering</h3>
<p>Once the data is indexed, we need to filter search results based on the user’s location. The following code snippet demonstrates how to modify the Search API SDK to include geospatial filters:</p>
<Code language="tsx">
{`useSearchResults<ArticleModel, InitialState>({
  query: (query): any => {
    const latitude = 40.2404; // User's latitude
    const longitude = -75.1652; // User's longitude
    const radius = '100km'; // Search radius (100 km)
    const sourceId = '000000'; // Source ID of PUSH API

<pre><code>const request = query.getRequest();
const geofilter = new FilterGeo(&#39;coordinates&#39;, radius, latitude, longitude).toJson();
geofilter[&#39;type&#39;] = &#39;geoDistance&#39;;

// Conditionally add geo filter if coordinates are available
if (latitude &amp;&amp; longitude) {
  request.setSearchFilter(
    new FilterAnd([new FilterEqual(&#39;name&#39;, &#39;Dr. John Doe&#39;), geofilter])
  );
}

// Conditionally add source ID of PUSH Source
if (sourceId) {
  request.setSources([sourceId]);
}
</code></pre>
<p>  }
});`}
</Code></p>
<p>In this code, the <code>latitude</code> and <code>longitude</code> values represent the location of the user making the search. The <code>FilterGeo</code> function is used to filter the search results by geospatial data, with the <code>geoDistance</code> filter type ensuring that results are ordered based on proximity.</p>
<h2 id="why-geospatial-search-is-important">Why Geospatial Search is Important</h2>
<p>Geospatial search is particularly valuable for businesses that depend on location-specific data. Here are some key reasons why implementing geospatial search can enhance your business:</p>
<ol>
<li><p><strong>Proximity-Based Results</strong>: Geospatial search allows users to find nearby services, stores, or content based on their current location, improving the overall user experience.</p>
</li>
<li><p><strong>Personalized User Experience</strong>: By integrating location-based filtering, you can provide more relevant search results, ensuring that users find exactly what they need, when they need it.</p>
</li>
<li><p><strong>Better Engagement</strong>: Providing users with content that’s relevant to their location can increase engagement, especially in industries such as retail and healthcare, where local presence is key.</p>
</li>
<li><p><strong>Operational Efficiency</strong>: Geospatial search can streamline business operations by allowing for easy display and management of localized content, improving service delivery.</p>
</li>
</ol>
<h2 id="conclusion">Conclusion</h2>
<p>Geospatial search is a powerful feature in Sitecore that enhances the user experience by delivering more relevant and localized content. Whether you&#39;re working in retail, healthcare, or any other industry that relies on geographic data, implementing geospatial search can make a significant impact on your site&#39;s search functionality.</p>
<p>By leveraging Sitecore’s search API and incorporating geo filters into your queries, you can provide users with a more personalized and location-aware experience. As businesses continue to rely on geospatial data, integrating such features into your digital solutions will only become more critical for success.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Sitecore Performance Optimization Configuration</title>
      <link>https://alikhan.tech/blogs/2024/sitecore-optimizations</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/sitecore-optimizations</guid>
      <pubDate>Mon, 09 Sep 2024 03:00:00 GMT</pubDate>
      <description><![CDATA[When configuring a Sitecore instance for a production environment, performance optimization is critical to ensure smooth operation under heavy traffic and usage. As a Sitecore developer, I often configure the settings below to enhance performance, particularly for high-demand…]]></description>
      <content:encoded><![CDATA[<p>When configuring a Sitecore instance for a production environment, performance optimization is critical to ensure smooth operation under heavy traffic and usage. As a Sitecore developer, I often configure the settings below to enhance performance, particularly for high-demand sites. These settings are designed to manage memory usage, reduce unnecessary resource consumption, and fine-tune how Sitecore handles tasks like caching, analytics, and content management. By adjusting cache sizes, disabling certain features, and refining other key configurations, we can ensure that Sitecore performs efficiently while balancing resource utilization.</p>
<p>This configuration can be utilized to improve the performance of Sitecore across various environments, ensuring that production instances are set up to deliver optimal results.</p>
<Code language="xml">
{`<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:env="http://www.sitecore.net/xmlconfig/env/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
    <sitecore>

<pre><code>    &lt;databases&gt;
        &lt;database id=&quot;web&quot; role:require=&quot;ContentDelivery or Standalone&quot;&gt;
            &lt;securityEnabled&gt;false&lt;/securityEnabled&gt;
            &lt;cacheSizes&gt;
                &lt;data&gt;1000MB&lt;/data&gt;
                &lt;items&gt;1000MB&lt;/items&gt;
            &lt;/cacheSizes&gt;
        &lt;/database&gt;

        &lt;database id=&quot;master&quot; role:require=&quot;Standalone or ContentManagement&quot;&gt;
            &lt;cacheSizes&gt;
                &lt;data&gt;1000MB&lt;/data&gt;
                &lt;items&gt;1000MB&lt;/items&gt;
            &lt;/cacheSizes&gt;
        &lt;/database&gt;
    &lt;/databases&gt;
    
    &lt;settings&gt;
        &lt;setting name=&quot;Analytics.AutoDetectBots&quot; value=&quot;true&quot; /&gt;
        &lt;setting name=&quot;Analytics.Robots.IgnoreRobots&quot; value=&quot;true&quot; /&gt;
        &lt;setting name=&quot;ContentTesting.AutomaticContentTesting.Enabled&quot; value=&quot;false&quot; /&gt;
        &lt;setting name=&quot;Publishing.DeepScanRelatedItems&quot; value=&quot;false&quot;/&gt;
        &lt;setting name=&quot;Caching.AccessResultCacheSize&quot; value=&quot;200MB&quot; /&gt;
        &lt;setting name=&quot;Counters.Enabled&quot; set:value=&quot;false&quot; /&gt;
        &lt;setting name=&quot;ContentEditor.CheckHasChildrenOnTreeNodes&quot; set:value=&quot;false&quot; /&gt;
        &lt;setting name=&quot;ContentEditor.CheckSecurityOnTreeNodes&quot; set:value=&quot;false&quot; /&gt;
        &lt;setting name=&quot;Pipelines.Profiling.Enabled&quot; set:value=&quot;false&quot; /&gt;
        &lt;setting name=&quot;Analytics.CookieLifetime&quot; set:value=&quot;34310&quot;/&gt;

        &lt;setting name=&quot;HtmlEditor.RemoveScripts&quot; &gt;
            &lt;patch:attribute name=&quot;value&quot;&gt;false&lt;/patch:attribute&gt;
        &lt;/setting&gt;

        &lt;setting name=&quot;GeoIp.PerformLookup&quot;&gt;
            &lt;patch:attribute name=&quot;value&quot; value=&quot;false&quot; /&gt;
        &lt;/setting&gt;
    &lt;/settings&gt;
    
    &lt;scheduling&gt;
        &lt;agent type=&quot;Sitecore.Tasks.CounterDumpAgent&quot;&gt;
            &lt;patch:delete /&gt;
        &lt;/agent&gt;
        &lt;frequency&gt;02:00:00&lt;/frequency&gt;
    &lt;/scheduling&gt;
    
    &lt;hooks&gt;
        &lt;hook type=&quot;Sitecore.Diagnostics.HealthMonitorHook, Sitecore.Kernel&quot;&gt;
            &lt;patch:delete /&gt;
        &lt;/hook&gt;
        &lt;hook type=&quot;Sitecore.Diagnostics.MemoryMonitorHook, Sitecore.Kernel&quot;&gt;
            &lt;patch:delete /&gt;
        &lt;/hook&gt;
    &lt;/hooks&gt;
    
&lt;/sitecore&gt;
</code></pre>
<p></configuration>`}
</Code></p>
<h2 id="configuration-breakdown">Configuration Breakdown</h2>
<h3 id="database-settings">Database Settings</h3>
<ul>
<li><p><strong><code>&lt;securityEnabled&gt;false&lt;/securityEnabled&gt;</code> in <code>&lt;database id=&quot;web&quot;&gt;</code></strong><br>Disables security checks on the <code>web</code> database, which can improve performance by bypassing permission evaluations for public content.</p>
<ul>
<li><strong>Pros</strong>: Increases speed for content retrieval.</li>
<li><strong>Cons</strong>: May pose a risk if sensitive content is mistakenly published.</li>
</ul>
</li>
<li><p><strong><code>&lt;cacheSizes&gt;</code> in <code>&lt;database id=&quot;web&quot;&gt;</code> and <code>&lt;database id=&quot;master&quot;&gt;</code></strong><br>Sets cache sizes for data and items on both databases, optimizing retrieval times.</p>
<ul>
<li><strong>Pros</strong>: Enhances performance by reducing database calls.</li>
<li><strong>Cons</strong>: High memory usage may require monitoring to avoid memory issues.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="sitecore-settings">Sitecore Settings</h3>
<ul>
<li><p><strong><code>&lt;setting name=&quot;Analytics.AutoDetectBots&quot; value=&quot;true&quot; /&gt;</code></strong><br>Enables bot detection, filtering bot activity in analytics.</p>
<ul>
<li><strong>Pros</strong>: Improves analytics accuracy.</li>
<li><strong>Cons</strong>: Could misclassify some legitimate users as bots.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;Analytics.Robots.IgnoreRobots&quot; value=&quot;true&quot; /&gt;</code></strong><br>Ignores bot activity in analytics reports.</p>
<ul>
<li><strong>Pros</strong>: Maintains data integrity by excluding bots.</li>
<li><strong>Cons</strong>: Risk of overlooking user behavior if real users are misclassified as bots.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;ContentTesting.AutomaticContentTesting.Enabled&quot; value=&quot;false&quot; /&gt;</code></strong><br>Disables automatic content testing.</p>
<ul>
<li><strong>Pros</strong>: Reduces resource use by disabling unused features.</li>
<li><strong>Cons</strong>: Limits A/B testing capabilities.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;Publishing.DeepScanRelatedItems&quot; value=&quot;false&quot;/&gt;</code></strong><br>Prevents deep scans of related items during publishing, which can enhance publish speed.</p>
<ul>
<li><strong>Pros</strong>: Shortens publishing times.</li>
<li><strong>Cons</strong>: Potentially omits related items if dependencies exist.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;Caching.AccessResultCacheSize&quot; value=&quot;200MB&quot; /&gt;</code></strong><br>Allocates 200MB to the access result cache, helping cache permission checks.</p>
<ul>
<li><strong>Pros</strong>: Speeds up access calculations.</li>
<li><strong>Cons</strong>: Higher memory use.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;Counters.Enabled&quot; set:value=&quot;false&quot; /&gt;</code></strong><br>Disables Sitecore counters, which track various metrics.</p>
<ul>
<li><strong>Pros</strong>: Frees up resources, optimizing performance.</li>
<li><strong>Cons</strong>: Reduces ability to monitor performance metrics.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;ContentEditor.CheckHasChildrenOnTreeNodes&quot; set:value=&quot;false&quot; /&gt;</code></strong><br>Disables child node checks in the Content Editor, reducing rendering time.</p>
<ul>
<li><strong>Pros</strong>: Speeds up Content Editor operations.</li>
<li><strong>Cons</strong>: Tree view may lack structure information, which could impact navigation.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;ContentEditor.CheckSecurityOnTreeNodes&quot; set:value=&quot;false&quot; /&gt;</code></strong><br>Disables security checks on tree nodes in Content Editor.</p>
<ul>
<li><strong>Pros</strong>: Enhances performance for content editors.</li>
<li><strong>Cons</strong>: Security risks if sensitive content permissions are ignored.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;Pipelines.Profiling.Enabled&quot; set:value=&quot;false&quot; /&gt;</code></strong><br>Disables pipeline profiling, saving resources by not tracking processing performance.</p>
<ul>
<li><strong>Pros</strong>: Improves response time and reduces load.</li>
<li><strong>Cons</strong>: Limited visibility for pipeline-related debugging.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;Analytics.CookieLifetime&quot; set:value=&quot;34310&quot;/&gt;</code></strong><br>Sets the duration for analytics cookies.</p>
<ul>
<li><strong>Pros</strong>: Manages user session persistence.</li>
<li><strong>Cons</strong>: Shorter cookie lifetimes may impact tracking consistency.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;HtmlEditor.RemoveScripts&quot;&gt;&lt;patch:attribute name=&quot;value&quot;&gt;false&lt;/patch:attribute&gt;&lt;/setting&gt;</code></strong><br>Allows scripts to be included in HTML editor content.</p>
<ul>
<li><strong>Pros</strong>: Supports custom scripting in content.</li>
<li><strong>Cons</strong>: Potential security risks if scripts are not sanitized.</li>
</ul>
</li>
<li><p><strong><code>&lt;setting name=&quot;GeoIp.PerformLookup&quot;&gt;&lt;patch:attribute name=&quot;value&quot; value=&quot;false&quot; /&gt;&lt;/setting&gt;</code></strong><br>Disables GeoIP lookup, which can reduce load from location-based lookups.</p>
<ul>
<li><strong>Pros</strong>: Enhances performance by avoiding extra lookups.</li>
<li><strong>Cons</strong>: Location-based content may be impacted if needed.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="scheduling">Scheduling</h3>
<ul>
<li><strong>Counter Dump Agent Deletion</strong><br><strong><code>&lt;agent type=&quot;Sitecore.Tasks.CounterDumpAgent&quot;&gt;&lt;patch:delete /&gt;&lt;/agent&gt;</code></strong><br>Removes the <code>CounterDumpAgent</code> to reduce processing overhead.<ul>
<li><strong>Pros</strong>: Reduces load by eliminating non-essential monitoring.</li>
<li><strong>Cons</strong>: Reduces insight into counters.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="hooks">Hooks</h3>
<ul>
<li><p><strong>Health Monitor Hook Deletion</strong><br><strong><code>&lt;hook type=&quot;Sitecore.Diagnostics.HealthMonitorHook, Sitecore.Kernel&quot;&gt;&lt;patch:delete /&gt;&lt;/hook&gt;</code></strong><br>Removes Health Monitor Hook, saving resources.</p>
<ul>
<li><strong>Pros</strong>: Reduces resource consumption.</li>
<li><strong>Cons</strong>: Misses out on automated health monitoring.</li>
</ul>
</li>
<li><p><strong>Memory Monitor Hook Deletion</strong><br><strong><code>&lt;hook type=&quot;Sitecore.Diagnostics.MemoryMonitorHook, Sitecore.Kernel&quot;&gt;&lt;patch:delete /&gt;&lt;/hook&gt;</code></strong><br>Disables Memory Monitor Hook to avoid memory monitoring overhead.</p>
<ul>
<li><strong>Pros</strong>: Saves resources.</li>
<li><strong>Cons</strong>: Undetected memory issues could affect performance.</li>
</ul>
</li>
</ul>
<hr>
<p>Ensure to thoroughly test configurations before production deployment, especially if disabling features essential for monitoring or analytics.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Exposing Site Settings from the Settings item in a Headless JSS Next.js Application</title>
      <link>https://alikhan.tech/blogs/2024/expose-site-settings-in-headless-nextjs</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/expose-site-settings-in-headless-nextjs</guid>
      <pubDate>Fri, 16 Aug 2024 03:00:00 GMT</pubDate>
      <description><![CDATA[In this blog post, we will explore a common requirement in Sitecore Headless SXA, exposing site-specific settings and global data to the front end when working with a JSS headless site. Problem Statement When building headless JSS sites, you often need to expose certain site…]]></description>
      <content:encoded><![CDATA[<p>In this blog post, we will explore a common requirement in Sitecore Headless SXA, exposing site-specific settings and global data to the front end when working with a JSS headless site.</p>
<h2 id="problem-statement">Problem Statement</h2>
<p>When building headless JSS sites, you often need to expose certain site settings or global configuration data from the Sitecore SXA Settings node to the front-end. However, the out-of-the-box Sitecore Layout Service primarily focuses on delivering route details (placeholders, data sources, fields, etc.), which may not fully cover the broader configuration needs.</p>
<p>In such cases, using GraphQL to query site-specific settings and integrating the results into the page-props is a reliable approach.</p>
<h3 id="the-solution">The Solution</h3>
<p>To address this requirement, we can extend the Sitecore page-props to include site-specific settings. The approach involves:</p>
<ol>
<li>Extending the <code>SitecorePageProps</code> type to include site settings.</li>
<li>Creating a GraphQL service to query and fetch the settings items.</li>
<li>Adding a new plugin to the page-props-factory to populate the site settings into the props for every page.</li>
</ol>
<p>Let&#39;s walk through the implementation step-by-step.</p>
<h2 id="implementation-steps">Implementation Steps</h2>
<h3 id="step-1-extend-the-sitecorepageprops-type">Step 1: Extend the <code>SitecorePageProps</code> Type</h3>
<p>The first step is to extend the <code>SitecorePageProps</code> type to include a new property for site settings. We modify the <code>src/types/props/page-props.ts</code> file and add a new variable:</p>
<Code language="typescript">
{`export type SitecorePageProps = {
  site: SiteInfo;
  locale: string;
  dictionary: DictionaryPhrases;
  componentProps: ComponentPropsCollection;
  notFound: boolean;
  layoutData: LayoutServiceData;
  headLinks: HTMLLink[];
  SiteConfigurations: SiteSettingFields;
};`}
</Code>

<p>Here, we introduce a new property called <code>SiteConfigurations</code> of type <code>SiteSettingFields</code>, which will hold the site settings data.</p>
<h3 id="step-2-create-a-graphql-service-to-fetch-settings">Step 2: Create a GraphQL Service to Fetch Settings</h3>
<p>Next, we create a service to handle GraphQL queries and fetch the settings fields. We’ll create a new file called <code>settings-service.ts</code> inside <code>src/lib/page-props-factory/services</code>.</p>
<Code language="jsx">
{`import { GraphQLClient } from '@sitecore-jss/sitecore-jss';
import { debug, GraphQLLayoutService, GraphQLLayoutServiceConfig, LayoutServiceData } from '@sitecore-jss/sitecore-jss-nextjs';

<p>export class GraphQLSettingsService extends GraphQLLayoutService {
  private myGraphQLClient: GraphQLClient;</p>
<p>  constructor(public serviceConfig: GraphQLLayoutServiceConfig) {
    super(serviceConfig);
    this.myGraphQLClient = this.getGraphQLClient();
  }</p>
<p>  public async fetchSettings(siteName: string, language: string): Promise<LayoutServiceData> {
    const query = this.getQuery(siteName, language);</p>
<pre><code>const data = await this.myGraphQLClient.request&lt;{
  sitesettings: {
    item: {
      contentRoot: {
        children: SiteSettingFields;
      };
    };
  };
}&gt;(query);

return data.item.rendered;
</code></pre>
<p>  }</p>
<p>  private getQuery(siteName: string, language: string) {
    return `query {
      sitesettings: layout(site: &quot;${siteName}&quot;, routePath: &quot;/&quot;, language: &quot;${language}&quot;) {
        item {
          contentRoot: parent {
            children(includeTemplateIDs: &quot;{54BC031B-293F-46F6-8F98-CB4A9B37BDE2}&quot;) {
              results {
                sampleField: field(name: &quot;sampleField&quot;) {
                  jsonValue
                }
              }
            }
          }
        }
      }
    }`;
  }
}`}
</Code></p>
<p>In this example:</p>
<ul>
<li>We use the <code>GraphQLClient</code> to execute a query against the Sitecore GraphQL endpoint.</li>
<li>The query retrieves the children of the settings node, filtering by template ID (<code>{54BC031B-293F-46F6-8F98-CB4A9B37BDE2}</code>) and selecting the relevant fields.</li>
<li>The <code>sampleField</code> is an example field from the settings that we want to expose.</li>
</ul>
<p>This service is designed to fetch the site settings from the settings node, specifically tailored for each site.</p>
<h3 id="step-3-add-a-new-plugin-to-the-page-props-factory">Step 3: Add a New Plugin to the Page-Props Factory</h3>
<p>Now, we integrate the site settings into our page-props using a custom plugin. We create a file called <code>get-settings.ts</code> inside <code>src/lib/page-props-factory/plugins</code>:</p>
<Code language="typescript">
{`import { GetStaticPropsContext, GetServerSidePropsContext } from 'next';
import config from 'temp/config';
import { Plugin } from '..';
import { GraphQLSettingsService } from '../services/settings-service';
import { SitecorePageProps } from 'lib/page-props';
import clientFactory from 'lib/graphql-client-factory';

<p>class GetSiteSettingsPlugin implements Plugin {
  private settingsRequestClient: GraphQLSettingsService;</p>
<p>  constructor() {
    const siteName = config.sitecoreSiteName;
    this.settingsRequestClient = new GraphQLSettingsService({
      siteName,
      clientFactory,
      retries: (process.env.GRAPH_QL_SERVICE_RETRIES &amp;&amp; parseInt(process.env.GRAPH_QL_SERVICE_RETRIES, 10)) as number,
    });
  }</p>
<p>  order = 2;</p>
<p>  async exec(props: SitecorePageProps, context: GetServerSidePropsContext | GetStaticPropsContext) {
    const siteSettings = await this.settingsRequestClient.fetchSettings(
      props?.site?.name,
      props.locale
    );</p>
<pre><code>if (siteSettings != null &amp;&amp; props != null) {
  props.SiteConfigurations = siteSettings ?? {};
}

return props;
</code></pre>
<p>  }
}</p>
<p>export const getSiteSettingsPlugin = new GetSiteSettingsPlugin();`}
</Code></p>
<h3 id="plugin-explanation">Plugin Explanation</h3>
<ul>
<li><p><strong>Order Property</strong>: The <code>order = 2</code> ensures that this plugin runs after the core plugins (e.g., <code>normal-mode</code> and <code>preview-mode</code>). This is crucial because it allows us to populate the settings data before any layout data is processed.</p>
</li>
<li><p><strong>Settings Fetching</strong>: We call the <code>fetchSettings</code> method to retrieve the settings data based on the site name and locale. The resulting data is stored in the <code>SiteConfigurations</code> property within <code>props</code>, making it accessible across the application.</p>
</li>
</ul>
<h3 id="optimization-considerations">Optimization Considerations</h3>
<p>Since this GraphQL query runs on every page render, caching the results is recommended to avoid redundant requests and improve performance. Implementing caching at either the service or plugin level can significantly optimize the solution.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This approach gives you a clean and extensible way to expose site settings from the SXA settings node to your Next.js frontend. By integrating GraphQL queries into the page-props factory and leveraging custom plugins, we can make site configurations accessible across every page in the application.</p>
<p>Feel free to enhance this solution further with caching strategies or by extending the settings to include more complex data structures. Happy coding!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Handling Sitecore CM and IdentityServer Behind a Proxy or CDN in an Azure Environment</title>
      <link>https://alikhan.tech/blogs/2024/handling-cm-and-identityserver-behind-AGW</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/handling-cm-and-identityserver-behind-AGW</guid>
      <pubDate>Wed, 17 Jul 2024 03:00:00 GMT</pubDate>
      <description><![CDATA[Recently, while working with a Sitecore 10.4 deployment in an Azure environment, we encountered an interesting challenge: handling Sitecore Content Management (CM) and IdentityServer (SI) behind a proxy/CDN setup using Azure Application Gateway. The DNS configuration was managed…]]></description>
      <content:encoded><![CDATA[<p>Recently, while working with a Sitecore 10.4 deployment in an Azure environment, we encountered an interesting challenge: handling Sitecore Content Management (CM) and IdentityServer (SI) behind a proxy/CDN setup using Azure Application Gateway. The DNS configuration was managed at the Application Gateway level. Here’s how we resolved the issue.</p>
<h3 id="the-setup">The Setup</h3>
<p>In this scenario, the public URLs were routed through Azure Application Gateway before reaching the internal Sitecore servers:</p>
<ul>
<li><strong>User Request (CM):</strong> <code>https://cm.cdn.xyz.com</code>  → Azure Application Gateway → <code>https://cm.xyz.com</code> (internal CM URL)</li>
</ul>
<p>However, this setup presented some issues, as Sitecore’s out-of-the-box configuration isn’t fully equipped to handle such complex routing seamlessly.</p>
<h3 id="issue">Issue</h3>
<p><strong>Misalignment in URLs:</strong> The login redirects and callbacks during authentication often landed on the internal URLs (<code>https://cm.xyz.com</code>) rather than staying within the proxy URLs (<code>https://cm.cdn.xyz.com</code>), and hence produces invalid request error.</p>
<p><img src="https://alikhan.tech/images/post/sc-si-auth-error.png" alt="image"></p>
<h3 id="the-three-step-solution">The Three-Step Solution</h3>
<p>To resolve these issues, a three-step solution was implemented:</p>
<h4 id="1-install-the-application-gateway-host-rewrite-extension">1. <strong>Install the &quot;Application Gateway Host Rewrite&quot; Extension</strong></h4>
<p>The first step is to ensure that the host headers are correctly rewritten so that Sitecore picks up the appropriate URLs. We installed the <strong>Application Gateway Host Rewrite</strong> extension on the CM App Service. This extension allows the Azure Application Gateway to rewrite the host headers for incoming requests, ensuring that IdentityServer4 picks up the correct host (<code>https://cm.cdn.xyz.com</code>), even after the requests have passed through the gateway.</p>
<h4 id="2-update-sitecore-identity-server-confguration">2. <strong>Update Sitecore Identity Server confguration</strong></h4>
<p>The next step involves updating the Sitecore Identity Server configurations to allow communication from all relevant domains.</p>
<ol>
<li>Open the <code>\Config\production\Sitecore.IdentityServer.Host.xml</code> file in the Sitecore Identity Server site root.</li>
<li>Locate the <code>&lt;AllowedCorsOrigins&gt;</code> tag and update it to include all the necessary domains that need to communicate with IdentityServer. Separate each domain using the <code>|</code> symbol.</li>
</ol>
<p>For example, in an Azure QA environment with both CM and CD app services, the configuration might look like this:</p>
<Code language="xml">
{`<AllowedCorsOrigins>
  <AllowedCorsOriginsGroup1>https://cm.xyz.com|https://cm.cdn.xyz.com</AllowedCorsOriginsGroup1>
</AllowedCorsOrigins>`}
</Code>

<h4 id="3-add-a-rewrite-rule-in-cms-webconfig">3. <strong>Add a Rewrite Rule in CM’s <code>web.config</code></strong></h4>
<p>Lastly, we added a rewrite rule in the <code>web.config</code> of the Content Management instance to handle URL rewrites correctly for outbound responses. Here’s the rule that was applied:</p>
<Code language="xml">
{`<system.webServer>
  <rewrite>
    <outboundRules>
      <rule name="HttpToHttpsOutbound" preCondition="IsRedirect">
        <match serverVariable="RESPONSE_Location" pattern="^http://cm.xyz.com/(.*)" />
        <action type="Rewrite" value="https://cm.cdn.xyz.com/{R:1}" />
      </rule>
      <preConditions>
        <preCondition name="IsRedirect">
          <add input="{RESPONSE_STATUS}" pattern="3\d\d" />
        </preCondition>
      </preConditions>
    </outboundRules>
  </rewrite>
</system.webServer>`}
</Code>

<p>This rule ensures that any HTTP redirect responses are transformed to use the correct HTTPS endpoint (<code>https://cm.cdn.xyz.com</code>) and that the responses maintain consistency with the proxied URL.</p>
<h3 id="wrapping-up">Wrapping Up</h3>
<p>By leveraging the <strong>Application Gateway Host Rewrite</strong> extension and carefully configuring rewrite rules, we were able to seamlessly integrate Sitecore CM and IdentityServer in a complex proxy/CDN environment. These adjustments allowed the client’s Sitecore environment to maintain consistent URLs throughout the authentication flow, improving the user experience and eliminating issues caused by internal vs. external URL mismatches.</p>
<p>This setup highlights the importance of understanding how Sitecore’s configurations interact with reverse proxies and CDNs, especially when dealing with environments hosted in Azure.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Indexing Sitecore Content Hub Assets in Coveo: A Practical Guide</title>
      <link>https://alikhan.tech/blogs/2024/indexing-content-hub-in-coveo</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/indexing-content-hub-in-coveo</guid>
      <pubDate>Fri, 28 Jun 2024 01:00:00 GMT</pubDate>
      <description><![CDATA[In the composable world of modern Sitecore solutions, integrating search appliances like Coveo with digital asset management tools such as Sitecore Content Hub is common practice. One typical use case involves building Coveo search sources to index content stored in Sitecore…]]></description>
      <content:encoded><![CDATA[<p>In the composable world of modern Sitecore solutions, integrating search appliances like Coveo with digital asset management tools such as Sitecore Content Hub is common practice. One typical use case involves building Coveo search sources to index content stored in Sitecore Content Hub. These indexes can then be used to create rich front-end search experiences.</p>
<h3 id="use-case-indexing-approved-pdfs-with-public-urls">Use Case: Indexing Approved PDFs with Public URLs</h3>
<p>In this post, we explore a scenario where we needed to index only approved PDF documents from Content Hub that have public URLs. We leveraged the Scroll API to retrieve and process a large number of results efficiently.</p>
<h4 id="using-the-scroll-api">Using the Scroll API</h4>
<p>The Scroll API is a powerful tool for retrieving extensive results from a single query without the limitations of traditional pagination. Unlike standard queries, it does not support the <code>skip</code> parameter and relies on retrieving the next page through the resource link. This allows continuous paging until no more results are available.</p>
<p>Here’s the Scroll API query we used:</p>
<pre><code class="language-http">GET /api/entities/scroll?query=Definition.Name==&#39;M.Asset&#39; AND Parent(&#39;AssetMediaToAsset&#39;).id==1057 AND Parent(&#39;FinalLifeCycleStatusToAsset&#39;).id==544&amp;sort=createdOn&amp;order=Asc
</code></pre>
<p>This query retrieves assets defined as <code>&#39;M.Asset&#39;</code> and filters them to those associated with specific media and lifecycle status IDs.</p>
<h4 id="filtering-for-approved-pdfs">Filtering for Approved PDFs</h4>
<p>To further refine our results to only include approved PDF assets, we used the following filters:</p>
<ul>
<li><strong>PDF Assets</strong>: <code>Parent(&#39;AssetMediaToAsset&#39;).id==1057</code></li>
<li><strong>Approved Assets</strong>: <code>Parent(&#39;FinalLifeCycleStatusToAsset&#39;)==544</code></li>
</ul>
<p>Adding these filters to our query ensured that only approved PDF assets were returned. Although these IDs are typically standard across all Content Hub instances, it&#39;s important to verify them in your own Content Hub instance before using them in queries. You can locate the asset media type IDs under Taxonomy Management within Content Hub.</p>
<h4 id="retrieving-public-urls-of-an-asset">Retrieving Public URLs of an Asset</h4>
<p>To include the public URLs of the PDFs in the index, we had to call additional APIs. For each asset returned by the Scroll API, we used the following endpoint to get all API URLs of the public links:</p>
<pre><code class="language-http">GET /api/entities/{assetId}/relations/AssetToPublicLink
</code></pre>
<p>The response provides the actual API links needed to obtain the public URL. Here’s an example response:</p>
<pre><code class="language-json">{
    &quot;children&quot;: [
        {&quot;href&quot;: &quot;/api/entities/303872&quot;},
        {&quot;href&quot;: &quot;/api/entities/235003&quot;}
    ],
    &quot;inherits_security&quot;: true,
    &quot;self&quot;: {&quot;href&quot;: &quot;/api/entities/31999/relations/AssetToPublicLink&quot;}
}
</code></pre>
<p>We then called each API link to retrieve the public URL.</p>
<h3 id="indexing-data-into-coveo-using-push-apis">Indexing Data into Coveo Using Push APIs</h3>
<h4 id="step-1-create-the-file-container">Step 1: Create the File Container</h4>
<ol>
<li><p><strong>Obtain Access Token</strong>: Ensure it has <code>Privileges / Organization / Organization = View</code>.</p>
</li>
<li><p><strong>Gather Information</strong>:</p>
<ul>
<li><strong>OrganizationId</strong>: Found in your Coveo Platform.</li>
<li><strong>useVirtualHostedStyleUrl</strong>: Set to <code>true</code>.</li>
</ul>
</li>
<li><p><strong>Create File Container</strong>:</p>
<ul>
<li><strong>POST Request</strong>:<pre><code class="language-http">POST https://api.cloud.coveo.com/push/v1/organizations/&lt;OrganizationId&gt;/files?useVirtualHostedStyleUrl=true
Content-Type: application/json
Accept: application/json
Authorization: Bearer &lt;AccessToken&gt;
</code></pre>
</li>
<li><strong>Response</strong>: Contains <code>uploadUri</code>, <code>fileId</code>, <code>requiredHeaders</code>.</li>
</ul>
</li>
</ol>
<h4 id="step-2-upload-data-to-file-container">Step 2: Upload Data to File Container</h4>
<ol>
<li><p><strong>Prepare JSON Data</strong>: Structure your data using the <code>addOrUpdate</code> array.</p>
<pre><code class="language-json">{
    &quot;addOrUpdate&quot;: [
        {
            &quot;m_title&quot;: &quot;Example Title&quot;,
            &quot;m_overview&quot;: &quot;Description&quot;,
            &quot;documentId&quot;: &quot;http://example.com&quot;,
            &quot;data&quot;: &quot;Content&quot;,
            &quot;fileExtension&quot;: &quot;.txt&quot;
        }
    ]
}
</code></pre>
</li>
<li><p><strong>Upload Data</strong>:</p>
<ul>
<li><strong>PUT Request</strong>:<pre><code class="language-http">PUT &lt;uploadUri - From Step 1&gt;
Content-Type: application/octet-stream
x-amz-server-side-encryption: AES256
</code></pre>
</li>
<li><strong>Body</strong>: Your JSON data.</li>
</ul>
</li>
</ol>
<h4 id="step-3-push-file-container-to-push-source">Step 3: Push File Container to Push Source</h4>
<ol>
<li><p><strong>Push Data</strong>:</p>
<ul>
<li><strong>PUT Request</strong>:<pre><code class="language-http">PUT https://api.cloud.coveo.com/push/v1/organizations/&lt;OrganizationId&gt;/sources/&lt;SourceId&gt;/documents/batch?fileId=&lt;fileId&gt;
Content-Type: application/json
Authorization: Bearer &lt;AccessToken&gt;
</code></pre>
</li>
</ul>
</li>
<li><p><strong>Response</strong>: Should be <code>null</code> with a <code>202 Accepted</code> status if successful.</p>
</li>
</ol>
<h3 id="pseudo-implementation">Pseudo Implementation</h3>
<p>With the necessary data gathered, we could proceed to index the content into Coveo. Below is a sample implementation in C#:</p>
<Code language="c#">
{`public void Process(string apiUrl, bool recursive = true)
{
    // Calling scroll API, and parse the API response into relevant model
    var contentHubItems = GetContentHubAssets(apiUrl);

<pre><code>// Process each item in contentHubItems  
foreach (var item in contentHubItems)
{
    try
    {
        // Get Public link of the content hub Item
        var publicLink = GetAssetPublicLink(item.assetToPublicLink);
        
        // Parse content hub item required information to coveo model
        List&lt;CoveoItem&gt; coveoItemsModel = new List&lt;CoveoItem&gt;();
        CoveoItem coveoObject = GetCoveoObject(publicLink, item);
        coveoItemsModel.Add(coveoObject);
    }
    catch (Exception ex)
    {
        // Log Error
    }
}

// Try to push items to Coveo
if (coveoItemsModel.Count &gt; 0)
{
    var coveoIPushModel = new CoveoItemsModel
    {
        addOrUpdate = coveoItemsModel
    };
    
    var containerInfo = CoveoHelper.CreateFileContainer();
    CoveoHelper.UploadConetntsToContainer(containerInfo.uploadUri, containerInfo.requiredHeaders.XAmzServerSideEncryption, coveoIPushModel);
    CoveoHelper.PushContainerToSource(containerInfo.fileId, containerInfo.requiredHeaders.XAmzServerSideEncryption);
}

// Recursively process next page if recursive flag is true
if (recursive &amp;&amp; contentHubItems.next != null &amp;&amp; !string.IsNullOrEmpty(contentHubItems.next.href))
{
    Process(contentHubItems.next.href);
}
</code></pre>
<p>}`}
</Code></p>
<h3 id="conclusion">Conclusion</h3>
<p>By leveraging the Scroll API and additional API calls for public URLs, we were able to effectively index approved PDF documents from Sitecore Content Hub into Coveo. This approach ensures that only the necessary and approved assets are indexed, optimizing the search experience.</p>
<p>Feel free to use and adapt this approach for your own Sitecore Content hub and Coveo integrations. </p>
]]></content:encoded>
    </item>
    <item>
      <title>Sitecore Search SDK Starter Kit: Enhanced User Experience</title>
      <link>https://alikhan.tech/blogs/2024/sitecore-search-sdk-starter-kit-enhanced-user-experience</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/sitecore-search-sdk-starter-kit-enhanced-user-experience</guid>
      <pubDate>Thu, 02 May 2024 01:00:00 GMT</pubDate>
      <description><![CDATA[The Sitecore Search SDK Starter Kit provides a powerful foundation for developers to build robust and user-friendly search functionalities into their websites. This open-source project offers a significant advantage by streamlining the integration of Sitecore Search with React…]]></description>
      <content:encoded><![CDATA[<p>The Sitecore Search SDK Starter Kit provides a powerful foundation for developers to build robust and user-friendly search functionalities into their websites. This open-source project offers a significant advantage by streamlining the integration of Sitecore Search with React based applications. Recently, I have made few enhancements to the forked GitHub repository (<a href="https://github.com/Sitecore/Sitecore-Search-JS-SDK-Starter-Kit">https://github.com/Sitecore/Sitecore-Search-JS-SDK-Starter-Kit</a>) that aim to improve the overall search experience for end users and provide developers with a more efficient development workflow.</p>
<h3 id="modular-component-architecture-for-improved-maintainability-and-reusability">Modular Component Architecture for Improved Maintainability and Reusability</h3>
<p>A core aspect of the enhancements involves restructuring the codebase to utilize separate components for facets, pagination, sorting, and search results. This shift towards a modular component architecture offers several advantages:</p>
<p><img src="https://alikhan.tech/images/post/starter-kit-enhancements/modular-approach.png" alt="image"></p>
<h4 id="enhanced-maintainability">Enhanced Maintainability:</h4>
<p>Separating components fosters a well-organized codebase. This improves code readability and understanding, making it easier for developers to maintain and extend the functionalities in the future.</p>
<h4 id="increased-reusability">Increased Reusability:</h4>
<p>Individual components can be reused across different parts of your application. This promotes code consistency and reduces development time by eliminating the need to rewrite code for similar functionalities.</p>
<h3 id="facet-refinement">Facet Refinement</h3>
<p>Finding the exact product or information can be challenging, especially when dealing with a vast number of facets. To address this, I&#39;ve introduced &quot;show more&quot; and &quot;show less&quot; functionality for facets. This allows users to control the number of facets displayed at once, preventing them from feeling overwhelmed by an excessive number of options. Additionally, facet filtering has been implemented, enabling users to refine their search results with greater precision.</p>
<p><img src="https://alikhan.tech/images/post/starter-kit-enhancements/facets-improvement.png" alt="image"></p>
<p>This following component, FacetList, renders a list of facet values with filtering capabilities and a show more/show less feature. It receives props such as selectedFacetsToRender, facet, and facetsnumbertoshow, and dynamically filters the facet values based on user input. The component allows users to toggle the visibility of facet values and efficiently manage large lists by displaying only a subset at a time.</p>
<Code language="tsx">
{`import { CheckIcon } from '@radix-ui/react-icons';
import type { SearchResponseFacet } from '@sitecore-search/react';
import { AccordionFacets } from '@sitecore-search/ui';
import { useEffect, useState } from 'react';
import type { SelectedFacets } from '../Facets';
import { AccordionFacetsStyled } from '../styled';
export interface FacetListType {
  facet: SearchResponseFacet;
  selectedFacetsToRender: SelectedFacets[];
  facetsnumbertoshow: number;
}

<p>export const FacetList = ({ selectedFacetsToRender, facet, facetsnumbertoshow }: FacetListType) =&gt; {
  const isSelected = (facetID: string, facetValue: string) =&gt; {
    return (
      selectedFacetsToRender.filter(
        (x) =&gt; x.id === facetID &amp;&amp; x.values.map((q) =&gt; q.id).includes(facetValue)
      ).length &gt; 0
    );
  };</p>
<p>  const minNumberOfItemsToShow = facetsnumbertoshow ?? 5;
  const totalItems = facet.value.length;
  const [keyword, setKeyword] = useState(&#39;&#39;);
  const [filteredItems, setFilteredItems] = useState(facet.value);
  const [numberOfItemsToShow, setNumberOfItemsToShow] = useState(minNumberOfItemsToShow);</p>
<p>  useEffect(() =&gt; {
    // This will be called whenever parentProp changes (i.e., when the parent component rerenders)
    setFilteredItems(facet.value);
  }, [facet.value]);</p>
<p>  const handleInputChange = (event: { target: { value: string } }) =&gt; {
    const inputKeyword = event.target.value.toLowerCase();
    setKeyword(inputKeyword);</p>
<pre><code>// Filter the array based on the input keyword
const updatedItems = facet.value.filter((item) =&gt;
  item.text.toLowerCase().includes(inputKeyword)
);
setFilteredItems(updatedItems);
</code></pre>
<p>  };</p>
<p>  const handleShowMore = () =&gt; {
    setNumberOfItemsToShow((prevVisibleItems) =&gt; prevVisibleItems + minNumberOfItemsToShow);
  };</p>
<p>  const handleShowLess = () =&gt; {
    setNumberOfItemsToShow((prevVisibleItems) =&gt;
      Math.max(prevVisibleItems - minNumberOfItemsToShow, minNumberOfItemsToShow)
    );
  };</p>
<p>  return (
    &lt;&gt;
      &lt;AccordionFacetsStyled.Facet facetId={facet.name} key={facet.name}&gt;
        &lt;AccordionFacets.Content&gt;
          <div className="input_wrap">
            <input
              name={facet.name}
              placeholder="Type to filter..."
              id={facet.name}
              type="text"
              value={keyword}
              onChange={handleInputChange}
            />
          </div>
          &lt;AccordionFacetsStyled.ValueList&gt;
            {filteredItems.slice(0, numberOfItemsToShow).map((v, index) =&gt; (
              ...
            ))}
          &lt;/AccordionFacetsStyled.ValueList&gt;</p>
<pre><code>      {filteredItems.length &gt; minNumberOfItemsToShow &amp;&amp; (
        &lt;div className=&quot;retractable-actions&quot;&gt;
          {numberOfItemsToShow &lt; totalItems &amp;&amp; (
            &lt;a href=&quot;#&quot; className=&quot;retractable-toggle&quot;
              onClick={(e) =&gt; {
                e.preventDefault();
                handleShowMore();
              }} &gt; Show More{&#39; &#39;}
            &lt;/a&gt;
          )}
          &lt;br /&gt;
          {numberOfItemsToShow &gt; minNumberOfItemsToShow &amp;&amp; (
            &lt;a href=&quot;#&quot; className=&quot;retractable-toggle&quot;
              onClick={(e) =&gt; {
                e.preventDefault();
                handleShowLess();
              }} &gt; {&#39; &#39;} Show Less
            &lt;/a&gt;
          )}
        &lt;/div&gt;
      )}
    &lt;/AccordionFacets.Content&gt;
  &lt;/AccordionFacetsStyled.Facet&gt;
&lt;/&gt;
</code></pre>
<p>  );
};</p>
<p>export default FacetList;`}
</Code></p>
<h3 id="sharable-search-queries">Sharable Search Queries</h3>
<p>Imagine collaborating with colleagues on research or revisiting a complex search later. The implemented mechanism to append facet selections and page numbers to the query string makes this a reality. Users can now generate URLs that include preselected facets and specific pages, Simply revisit the shared URL, and your desired search criteria are instantly applied.</p>
<p><img src="https://alikhan.tech/images/post/starter-kit-enhancements/searh-query.png" alt="image"></p>
<p>Following is the function responsible to generate query string and append in Query string, called when any facet value is clicked.</p>
<p>This TypeScript function, manages the modification of facet-related search parameters in a URL query string. It parses the current query parameters, updates facet values based on user actions (such as selecting or deselecting options), and generates an updated URL with the modified parameters, ultimately navigating to the new URL</p>
<Code language="tsx">
{`const updateFacetsSearchParams = (params: any, remove = false) => {
    const queryParams = new URLSearchParams(location.search);
    const searchParamsObject = paramsToObject(queryParams.entries()) as any;
    let values = searchParamsObject[params.facetId]?.split(',') ?? [];
    values = values.filter((n: any) => n);
    if (remove || params.checked === false) {
      const index = values.indexOf(params.facetValueId);
      if (index > -1) {
        values.splice(index, 1);
      }
    } else {
      values.push(params.facetValueId);
    }
    if (values.length <= 0) {
      delete searchParamsObject[params.facetId];
    }
    delete searchParamsObject.page;
    searchParamsObject[params.facetId] = removeDuplicates(values).join(',');
    const updatedRelativeUrl = generateQueryString('/search', searchParamsObject);
    navigate(updatedRelativeUrl);
  };`}

</Code>

<h3 id="open-source-contribution-and-continued-development">Open-Source Contribution and Continued Development</h3>
<p>All of the enhancements mentioned above are included in a pull request submitted to the forked GitHub repository: <a href="https://github.com/mgnventures/Sitecore-Search-TS-SDK-Starter-Kit/pull/1">https://github.com/mgnventures/Sitecore-Search-TS-SDK-Starter-Kit/pull/1</a> . This pull request provides a detailed overview of the changes and facilitates the integration of these improvements into the forked Sitecore Search SDK Starter Kit.</p>
<p>I believe these enhancements will significantly improve the Sitecore Search SDK Starter Kit, providing developers with a more efficient development experience and empowering users with a more intuitive and productive search experience. If you&#39;re interested in learning more or contributing to this project, feel free to visit the GitHub repository.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Leveraging Sitecore Search SDK Starter Kit into Your Sitecore Next.js Solution [Part 2]</title>
      <link>https://alikhan.tech/blogs/2024/leveraging-sitecore-search-sdk-starter-kit</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/leveraging-sitecore-search-sdk-starter-kit</guid>
      <pubDate>Wed, 17 Apr 2024 01:00:00 GMT</pubDate>
      <description><![CDATA[In this blog post, I'll walk you through the process of smoothly leveraging the Search SDK Starter Kit into a Sitecore Next.js solution. Prerequisites Before proceeding, ensure the following prerequisites are met: A Sitecore search account with initial configuration set up on…]]></description>
      <content:encoded><![CDATA[<p>In this blog post, I&#39;ll walk you through the process of smoothly leveraging the Search SDK Starter Kit into a Sitecore Next.js solution.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>Before proceeding, ensure the following prerequisites are met:</p>
<ul>
<li>A Sitecore search account with initial configuration set up on Customer Engagement Console (CEC).</li>
<li>Sitecore headless Next.js solution running on your local environment.</li>
<li>Installation of necessary packages:</li>
</ul>
<Code language="jsx">
{`npm install --save @sitecore-search/react
npm install --save @sitecore-search/ui
npm install --save styled-components`}
</Code>

<h3 id="configuration-steps">Configuration Steps</h3>
<h4 id="1-create-source-in-cec">1. Create Source in CEC</h4>
<p>Follow the steps outlined in the <a href="https://doc.sitecore.com/search/en/users/search-user-guide/configuring-a-source.html">official documentation</a> to create a source in CEC.</p>
<h4 id="2-configure-suggestions-blocks">2. Configure Suggestions Blocks</h4>
<p>Ensure that suggestions blocks have a field named <code>title_context_aware</code>. Refer to the <a href="https://doc.sitecore.com/search/en/users/search-user-guide/create-auto-complete-functionality-using-suggestions-blocks.html">documentation</a> for configuration details.</p>
<h4 id="3-setup-sorting-options">3. Setup Sorting Options</h4>
<p>Configure sorting options to include <code>featured_desc</code> and <code>featured_asc</code> criteria as per the <a href="https://doc.sitecore.com/search/en/users/search-user-guide/walkthrough--adding-the-option-to-sort-by-top-views.html">documentation</a>.</p>
<h4 id="4-preconfigured-widgets">4. Preconfigured Widgets</h4>
<p>Ensure that the following widgets are created beforehand:</p>
<ul>
<li>Search Results Page</li>
<li>Search Results</li>
<li>Preview Search</li>
</ul>
<h3 id="code-leveraging-steps">Code Leveraging Steps</h3>
<ol>
<li>Download or clone the Sitecore Search SDK Starter Kit repository.</li>
<li>Copy the following folders from the starter kit&#39;s <code>src</code> folder to your Sitecore Next.js solution&#39;s <code>src</code> folder:<ul>
<li>Contexts</li>
<li>Data</li>
<li>Hocs</li>
<li>Hooks</li>
<li>Themes</li>
</ul>
</li>
</ol>
<p><img src="https://alikhan.tech/images/post/starter-kit/sk-1.jpg" alt="image"></p>
<ol start="3">
<li>Create a new folder named <code>widgets</code> in your solution and copy the <code>PreviewSearch</code> and <code>SearchResults</code> folder along with <code>Utils.tsx</code> file from the starter kit.</li>
</ol>
<p><img src="https://alikhan.tech/images/post/starter-kit/sk-2.jpg" alt="image"></p>
<ol start="4">
<li>We might face formatting issue in our copied tsx files, for this we can update <code>.prettierrc</code> file to handle formatting issues, set the <code>endOfLine</code> value to <code>auto</code></li>
</ol>
<p><img src="https://alikhan.tech/images/post/starter-kit/sk-3.jpg" alt="image"></p>
<ol start="5">
<li>Create environment variables in the <code>.env</code> file:</li>
</ol>
<Code language="dotenv">
{`NEXT_PUBLIC_SEARCH_CUSTOMER_KEY=<customer-key>
NEXT_PUBLIC_SEARCH_API_KEY=<api-key>
NEXT_PUBLIC_SEARCH_ENV=prod`}
</Code>

<ol start="6">
<li>Update the <code>_app.tsx</code> file to integrate the widget provider.</li>
</ol>
<p>Add necessary imports.</p>
<Code language='tsx'>
{`import { LanguageContext } from 'src/contexts/languageContext';
import useLanguage from 'src/hooks/useLanguage';`}
</Code>

<p>add following const in within the main function.
<Code language='tsx'>
{<code>const { language, setLanguage } = useLanguage();</code>}
</Code></p>
<p>Wrap language context and widget provider with <code>&lt;I18nProvider /&gt;</code> and Supply environment variables for customer key and API key, final outcome should look like as shown in the screenshot below:</p>
<Code language="jsx">
{`<LanguageContext.Provider value={{ language, setLanguage }}> 
  <WidgetsProvider 
    env={process.env.NEXT_PUBLIC_SEARCH_ENV as Environment} 
    customerKey={process.env.NEXT_PUBLIC_SEARCH_CUSTOMER_KEY} 
    apiKey={process.env.NEXT_PUBLIC_SEARCH_API_KEY} 
    publicSuffix={true}>
      <I18nProvider lngDict={dictionary} locale={pageProps.locale}> </I18nProvider> 
  </WidgetsProvider> 
</LanguageContext.Provider>`}
</Code>


<ol start="7">
<li><p>Replace references to <code>react-router-dom</code> with <code>@sitecore-jss/sitecore-jss-nextjs</code> in <code>Styled.ts</code> file under <code>SearchResults</code> widget folder.</p>
</li>
<li><p>Replace <code>useNavigate</code> import from <code>react-router-dom</code> with <code>next/router</code> and fix all references accordingly, whereever it is being used.</p>
</li>
<li><p>Create a Search rendering in Sitecore and implement it in your Next.js solution.</p>
<ul>
<li>Create a <code>Search.tsx</code> file under <code>components/search</code> folder.</li>
<li>Call the Search results widget in the <code>Search.tsx</code> file.</li>
<li>Wrap the export with <code>withPageTracking</code> hoc, this will enable tracking user analytics.</li>
<li>Example implementation:</li>
</ul>
</li>
</ol>
<Code language="jsx">
{`import withPageTracking from 'src/hocs/withPageTracking';
import SearchResults from 'src/widgets/SearchResults';

<p>const Search = (): JSX.Element =&gt; {
  return &lt;SearchResults key={&#39;search&#39;} rfkId=&quot;rfkid_7&quot; defaultKeyphrase={&#39;&#39;} /&gt;;
};</p>
<p>export default withPageTracking(Search);`}
</Code></p>
<p>Now, you&#39;re ready to drop the search component on any page from the Experience Editor and enjoy the final outcome if the source and facets are configured properly in the CEC portal.</p>
<p><img src="https://alikhan.tech/images/post/starter-kit/sk-5.jpg" alt="image"></p>
<h2 id="conclusion">Conclusion</h2>
<p>By following these steps, you can seamlessly leverage the Sitecore Search SDK Starter Kit into your Sitecore Next.js solution. With proper configuration and implementation, you can enhance search functionality and improve user experience, and update the styling of search components as per the site theme.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Coveo for Sitecore - Guide to Migrate Changes Between Cloud Organizations</title>
      <link>https://alikhan.tech/blogs/2024/guide-to-migrate-changes-between-cloud-organizations</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/guide-to-migrate-changes-between-cloud-organizations</guid>
      <pubDate>Tue, 02 Apr 2024 03:00:00 GMT</pubDate>
      <description><![CDATA[Moving to a new Coveo Cloud organization can be a strategic step for your business. But what about the customizations and data you've built up in your dev or QA organization? This post will guide you through the process of migrating changes between Coveo cloud organizations.…]]></description>
      <content:encoded><![CDATA[<p>Moving to a new Coveo Cloud organization can be a strategic step for your business. But what about the customizations and data you&#39;ve built up in your dev or QA organization? This post will guide you through the process of migrating changes between Coveo cloud organizations.</p>
<h3 id="understanding-the-need-for-migration">Understanding the Need for Migration</h3>
<ul>
<li>Collaboration between developers and system administrators often involves experimentation and configuration changes.</li>
<li>As deployments grow, efficient migration between Coveo organizations becomes crucial for consistency.</li>
<li>Replicating production settings for new development iterations requires a streamlined migration process.</li>
</ul>
<h3 id="migrating-your-configuration-with-resource-snapshots">Migrating Your Configuration with Resource Snapshots</h3>
<p>Coveo&#39;s Resource Snapshot feature allows you to take control of your migration. With snapshots, you can copy the configuration of various resources, including:</p>
<ul>
<li>Query pipelines</li>
<li>Indexing pipeline extensions</li>
<li>Hosted search pages</li>
<li>Coveo Machine Learning models</li>
</ul>
<p>This lets you quickly set up your new organization with the same functionality as your dev or qa oraganizations.</p>
<h3 id="important-considerations-for-snapshots">Important Considerations for Snapshots</h3>
<p>While snapshots are a powerful tool, it&#39;s important to understand their limitations:</p>
<ul>
<li>Not all resource types are currently supported.</li>
<li>Coveo does not offer an automated process to migrate organization data (such as user data).</li>
<li>The snapshot can only copy the top 1,000 query suggestions from your old organization.</li>
<li>Data like Usage Analytics cannot be migrated.</li>
</ul>
<h3 id="professional-services-a-seamless-migration-option">Professional Services: A Seamless Migration Option</h3>
<p>For a more comprehensive migration experience, consider leveraging Coveo Professional Services. Coveo team can handle the entire process, ensuring a smooth transition with minimal disruption.</p>
<h4 id="creating-a-snapshot">Creating a Snapshot</h4>
<ul>
<li>Begin by creating a snapshot of resources in the source organization to capture the current configurations.</li>
</ul>
<p><img src="https://alikhan.tech/images/post/coveo-create-snapshot.png" alt="image"></p>
<ul>
<li>Choose to include dependencies automatically to ensure all necessary resources are captured.</li>
<li>Provide a descriptive name for easy identification and proceed to export the snapshot.</li>
</ul>
<p><img src="https://alikhan.tech/images/post/coveo-add-resources-to-snapshot.png" alt="image"></p>
<h4 id="exporting-and-applying-the-snapshot">Exporting and Applying the Snapshot</h4>
<ul>
<li>Export the created snapshot to the destination Coveo organization.</li>
<li>Switch to the destination organization and apply the snapshot.</li>
<li>Validate the snapshot to ensure all prerequisites are met and review any detected errors.</li>
<li>Address errors before proceeding with the snapshot application.</li>
</ul>
<h4 id="under-the-hood-snapshot-creation-and-application">Under the Hood: Snapshot Creation and Application</h4>
<ul>
<li>A snapshot consists of a JSON file listing included resources and configurations.</li>
<li>Coveo matches resource names in the snapshot with IDs in the destination organization to determine actions.</li>
<li>This matching process ensures consistency and accuracy during snapshot application, preventing conflicts.</li>
</ul>
<h3 id="conclusion">Conclusion</h3>
<p>Efficiently migrating changes between cloud organizations in Coveo for Sitecore is essential for maintaining consistency and streamlining development workflows. By following these step-by-step instructions, organizations can replicate configurations across environments with ease, reducing manual effort and minimizing the risk of errors. </p>
<blockquote>
<p>For further information and detailed guidance on managing resource snapshots, refer to the official Coveo documentation. Additionally, Coveo representatives are available to assist with specific inquiries and support ongoing optimization efforts.</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Sitecore Search SDK Integration [Part 1]</title>
      <link>https://alikhan.tech/blogs/2024/sitecore-search-sdk-integration</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/sitecore-search-sdk-integration</guid>
      <pubDate>Fri, 29 Mar 2024 01:00:00 GMT</pubDate>
      <description><![CDATA[In today's web development landscape, delivering efficient and intuitive search functionality is crucial to provide an enhanced user experience for your visitors. With the advent of Sitecore Search, developers now have a powerful tool at their disposal to seamlessly integrate…]]></description>
      <content:encoded><![CDATA[<p>In today&#39;s web development landscape, delivering efficient and intuitive search functionality is crucial to provide an enhanced user experience for your visitors. With the advent of Sitecore Search, developers now have a powerful tool at their disposal to seamlessly integrate robust search capabilities into their Next.js applications.</p>
<p>Sitecore Search offers a comprehensive platform that allows you to incorporate predictive and personalized search experiences for your visitors. By leveraging the capabilities of Sitecore Search, you can request and retrieve data such as search results, facets, suggestions, and more, empowering you to build a highly tailored and relevant search experience.</p>
<h3 id="understanding-the-react-sdk">Understanding the React SDK</h3>
<h4 id="what-is-the-react-sdk">What is the React SDK?</h4>
<p>The React SDK from Sitecore Search is tailor-made for developers in the React ecosystem, including Next.js. It simplifies the integration of Sitecore Search into web apps, enriching the user experience with advanced search features. Developers can quickly enhance their applications with predictive search, personalized results, and more, thanks to the streamlined capabilities of the React SDK.</p>
<h4 id="key-components-of-the-react-sdk">Key Components of the React SDK</h4>
<p>The React SDK comprises two npm packages:</p>
<ol>
<li><p><strong>React Package</strong>: This package forms the foundation of the SDK, providing core functionality.</p>
</li>
<li><p><strong>UI Package</strong>: Housing a plethora of UI components, the UI package equips developers with various composable to construct diverse user experiences seamlessly.</p>
</li>
</ol>
<h3 id="getting-started-with-the-react-sdk">Getting Started with the React SDK</h3>
<h4 id="installation-and-configuration">Installation and Configuration</h4>
<p>To start utilizing the React SDK, we need to follow these fundamental steps:</p>
<ol>
<li>Install the two required npm packages: React Package and UI Package.<Code language="jsx">
{`npm install --save @sitecore-search/react
npm install --save @sitecore-search/ui	`}
</Code></li>
<li>Wrap the React application with the Widget Provider component, furnishing the SDK with essential functionality, usually in Next JS application _app.tsx file is the root of the application.</li>
</ol>
<Code language="jsx">
{`<WidgetsProvider
            env={process.env.NEXT_PUBLIC_SEARCH_ENV as any}
            customerKey={process.env.NEXT_PUBLIC_SEARCH_CUSTOMER_KEY}
            apiKey={process.env.NEXT_PUBLIC_SEARCH_API_KEY}
            publicSuffix={true}>
    <Component  {...rest}  />
</WidgetsProvider> `}
</Code>
3. Configure the Search Config object to establish the connection between the application and the designated search instance. It's recommended to store sensitive information, such as API keys, in environment variables for security purposes.

<Code language="JSX">
{`NEXT_PUBLIC_SEARCH_CUSTOMER_KEY=<customer-key>
NEXT_PUBLIC_SEARCH_API_KEY=<api-key>
NEXT_PUBLIC_SEARCH_ENV=prod`}
</Code>
### Building Search Functionality with the React SDK

<h4 id="creating-search-components">Creating Search Components</h4>
<p>Once the SDK is integrated into the React application, we can start building out search functionality, in order to communicate with Sitecore Search API we will use query hooks available in the JS SDK package, for search results we will be using  <strong>useSearchResults</strong> query hook, this hook is used request and retrieve data for search results widgets. page, keyphrase, selectedFacets, itemsPerPage, and sortType.</p>
<p>Following is the basic example of its utilization:</p>
<Code language="jsx">
{`import type { SearchResultsInitialState, SearchResultsStoreState, SearchResultsWidgetQuery } from '@sitecore-search/react';
import { WidgetDataType, useSearchResults, useSearchResultsSelectedFacets, widget } from '@sitecore-search/react';

<p>type ArticleModel = {
  id: string;
  type?: string;
  title?: string;
  name?: string;
  subtitle?: string;
  url?: string;
  description?: string;
  content_text?: string;
  image_url?: string;
  source_id?: string;
};</p>
<p>type ArticleSearchResultsProps = {
  defaultSortType?: SearchResultsStoreState[&#39;sortType&#39;];
  defaultPage?: SearchResultsStoreState[&#39;page&#39;];
  defaultItemsPerPage?: SearchResultsStoreState[&#39;itemsPerPage&#39;];
  defaultKeyphrase?: SearchResultsStoreState[&#39;keyphrase&#39;];
};</p>
<p>type InitialState = SearchResultsInitialState&lt;&#39;itemsPerPage&#39; | &#39;keyphrase&#39; | &#39;page&#39; | &#39;sortType&#39;&gt;;</p>
<p>export const SearchResultsComponent = ({
  defaultSortType = &#39;date_desc&#39;,
  defaultPage = 1,
  defaultKeyphrase = &#39;&#39;,
  defaultItemsPerPage = 10,
}: ArticleSearchResultsProps) =&gt; {
  const {
    widgetRef,
    actions: {
      onResultsPerPageChange,
      onPageNumberChange,
      onItemClick,
      onFilterClick,
      onSortChange,
      onFacetClick,
      onClearFilters,
    },
    state: { sortType, page, itemsPerPage },
    queryResult: {
      isLoading,
      isFetching,
      data: {
        total_item: totalItems = 0,
        sort: { choices: sortChoices = [] } = {},
        facet: facets = [],
        content: articles = [],
      } = {},
    },
  } = useSearchResults&lt;ArticleModel, InitialState&gt;({
    query: (query:SearchResultsWidgetQuery) =&gt; query,
    state: {
      sortType: defaultSortType,
      page: defaultPage,
      itemsPerPage: defaultItemsPerPage,
      keyphrase: defaultKeyphrase,
    },
  });
  const totalPages = Math.ceil(totalItems / itemsPerPage);
  const selectedSortIndex = sortChoices.findIndex((s:any) =&gt; s.name === sortType);
  const selectedFacetsFromApi = useSearchResultsSelectedFacets();
  if (isLoading) {
    return ( <div> ... </div> );
  }
  return ( <div ref={widgetRef}> ... </div> );
};</p>
<p>const SearchResultsStyledWidget = widget(SearchResultsComponent, WidgetDataType.SEARCH_RESULTS, &#39;content&#39;);</p>
<p>export default SearchResultsStyledWidget;
`}
</Code></p>
<p>in the above code line #69 is where our UI will be placed to create search experience like the implementation of facets, search results, pagination, we will disuss this in the next parts of the blog.</p>
<p>It is important to Convert a UI component into a widget component before adding it to pages or use.</p>
<blockquote>
<p>To convert a component into a widget component, pass the UI component to the  <code>widget</code></p>
</blockquote>
<h3 id="utilizing-supporting-resources">Utilizing Supporting Resources</h3>
<h4 id="documentation-and-sample-repositories">Documentation and Sample Repositories</h4>
<p>Sitecore Search offers a comprehensive suite of resources to help developers in their journey to develop search experience:</p>
<ol>
<li>Detailed SDK documentation and a Storybook instance for exploring UI components.</li>
</ol>
<p><a href="https://developers.sitecorecloud.io/search-sdk/react/latest/storybook/index.html?path=/story/introduction-introduction--page">https://developers.sitecorecloud.io/search-sdk/react/latest/storybook/index.html?path=/story/introduction-introduction--page</a></p>
<ol start="2">
<li>Sample repositories, including a starter kit and the Developer Portal repository, provide practical examples and insights.</li>
</ol>
<p><a href="https://github.com/Sitecore/Sitecore-Search-JS-SDK-Starter-Kit">https://github.com/Sitecore/Sitecore-Search-JS-SDK-Starter-Kit</a></p>
<h3 id="conclusion">Conclusion</h3>
<p>Incorporating Sitecore Search into React applications empowers developers to deliver enhanced search experiences, thereby elevating user satisfaction and engagement. By leveraging the React SDK and its accompanying resources, developers can streamline the implementation process and unlock the full potential of search functionality within their applications.</p>
<p>In the next blog post, I&#39;ll show you how to use the Sitecore Search SDK Starter Kit from <a href="https://github.com/Sitecore/Sitecore-Search-JS-SDK-Starter-Kit">GitHub repo</a> to our Sitecore Next.js solution. Stay tuned for simple steps to create a great search experience!</p>
]]></content:encoded>
    </item>
    <item>
      <title>SharePoint On-Prem WebPart Management with PowerShell</title>
      <link>https://alikhan.tech/blogs/2024/sharepoint-on-prem-webpart-management-with-powershell</link>
      <guid isPermaLink="true">https://alikhan.tech/blogs/2024/sharepoint-on-prem-webpart-management-with-powershell</guid>
      <pubDate>Thu, 04 Jan 2024 02:00:00 GMT</pubDate>
      <description><![CDATA[SharePoint On-Premises remains a cornerstone of many organizations' collaboration and content management strategies. Among its key features are WebParts, which allow users to customize SharePoint sites with various functionalities. However, managing WebParts in SharePoint…]]></description>
      <content:encoded><![CDATA[<p>SharePoint On-Premises remains a cornerstone of many organizations&#39; collaboration and content management strategies. Among its key features are WebParts, which allow users to customize SharePoint sites with various functionalities. However, managing WebParts in SharePoint On-Premises environments can be complex, especially across multiple farms or servers. Fortunately, PowerShell offers a robust set of tools to streamline the process. In this blog post, we&#39;ll explore how PowerShell can simplify the installation, updating, and uninstallation of SharePoint On-Premises WebParts, empowering administrators to efficiently manage their SharePoint deployments.</p>
<p>Installing SharePoint On-Premises WebParts with PowerShell:
Automating the installation of SharePoint On-Premises WebParts using PowerShell can significantly reduce administrative overhead. Below is a PowerShell script for installing a WebPart in a SharePoint On-Premises environment:</p>
<Code language="PowerShell">
{`Add-SPSolution "C:\Path\To\WebPart\File.wsp"
Install-SPSolution -Identity WebPartName.wsp –WebApplication <web application url> -GACDeployment
`}
</Code>

<p>This script adds the solution file containing the WebPart to the SharePoint farm&#39;s solution store and deploys it globally across the farm.</p>
<p>Updating SharePoint On-Premises WebParts with PowerShell:
Keeping SharePoint On-Premises WebParts up-to-date is essential for maintaining security and functionality. PowerShell can automate the process of updating WebParts across SharePoint farms. Here&#39;s a PowerShell script to update a SharePoint On-Premises WebPart:</p>
<Code language="PowerShell">
{`Update-SPSolution -Identity WebPartName.wsp -LiteralPath "C:\Path\To\Updated\WebPart\File.wsp" -GACDeployment
`}
</Code>

<p>This script updates the existing solution with the new version of the WebPart file.</p>
<p>Uninstalling SharePoint On-Premises WebParts with PowerShell:
When SharePoint On-Premises WebParts are no longer needed, removing them is crucial to maintain system cleanliness and performance. PowerShell offers a straightforward approach to uninstalling SharePoint WebParts. Here&#39;s a PowerShell script to uninstall a SharePoint On-Premises WebPart:</p>
<Code language="PowerShell">
{`Uninstall-SPSolution -Identity WebPartName.wsp 
Remove-SPSolution -Identity WebPartName.wsp 
`}
</Code>

<p>This script uninstalls the solution containing the WebPart from the SharePoint farm and removes it from the solution store.</p>
<p>Conclusion:
PowerShell is a powerful tool for managing SharePoint On-Premises WebParts, enabling administrators to automate the installation, updating, and uninstallation processes. By leveraging PowerShell scripts tailored to their specific needs, administrators can streamline WebPart management tasks, saving time and reducing the risk of errors. Whether deploying new WebParts, applying updates, or removing obsolete components, PowerShell provides a flexible and efficient solution for SharePoint On-Premises WebPart management.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
