Erwin Smit Blog
website·Indexed 30 days ago
https://www.erwinsmit.com/A technical blog by frontend and fullstack developer Erwin Smit, featuring guides and tutorials on Sitecore JSS, Next.js, Azure Functions, Azure Static Web Apps, and Apollo Federation.
What's inside Erwin Smit
- For a stable and popular foundation, use React as the UI framework and TypeScript for type safety. These are considered the industry standard choices with minimal viable alternatives for large-scale projects.
Choose a styling strategy compatible with React Server Components
Avoid traditional CSS-in-JS (like Emotion or styled-components) if the project requires Server-Side Generation or React Server Components, as these technologies are strongly discouraged for server-side use.
Alternative styling options include:
- Vanilla Extract: Recommended for those wanting TypeScript support and CSS Modules behavior without the runtime overhead of CSS-in-JS. It provides compile-time validation for theme variables.
- Tailwind CSS: Recommended for simplicity and rapid development within TSX files, though it lacks a compiler to validate class name spelling.
- SCSS with BEM: A traditional approach, though it may suffer from architectural degradation over time and lacks the developer experience of type-safe styling.
Host Sitecore JSS Next.js on Azure using YARP as a reverse proxy
To host a Sitecore JSS Next.js application on Azure without using Vercel or legacy Azure Functions proxies, use a .NET application with YARP (Yet Another Reverse Proxy). YARP allows most proxy rules to be configured viaappsettings.jsonwithout requiring extensive custom code. To serve static files from the Next.js build (such as the/_nextpath), the file share must be mounted in the .NET application using aPhysicalFileProvider.var nextMount = builder.Configuration.GetValue<string>("NextMount"); app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(nextMount), RequestPath = "/_next" });Apply development rules for scalable Sitecore JSS components
Follow these rules to maintain a clean separation between the design system and the CMS integration:
- Use Theme Variables: Avoid hard-coded spacing and colors. Use theme variables to allow components to be adjusted globally across different contexts.
- No Markup/Styling in the App Layer: Do not add class names or inline styles to React components within the Sitecore consuming Next.js apps. All styling must reside in the UI components package to ensure the Storybook design system remains the single source of truth.
- Use GraphQL Aliases: When writing integrated or connected GraphQL queries, use aliases. This ensures that if property names change in Sitecore, only the query needs updating, while the rendering component remains functional.
Initialize OrderCloud SDK in a Next.js application
Initialize the OrderCloud JavaScript SDK in_app.tsxto establish a connection and authenticate anonymously. This ensures OrderCloud functionality is available throughout the application.import { Configuration, Tokens, Auth } from "ordercloud-javascript-sdk"; (async function(){ const clientId = process.env.NEXT_PUBLIC_OC_CLIENT_ID || ""; const baseApiUrl = process.env.NEXT_PUBLIC_OC_BASE_API_URL; const scope = process.env.NEXT_PUBLIC_OC_SCOPE?.split(","); Configuration.Set({ clientID: clientId, baseApiUrl: baseApiUrl, cookieOptions: { prefix: "hds-nextjs", path: "/" }, }); const authResponse = await Auth.Anonymous(clientId, scope || ([""] as ApiRole[])); Tokens.SetAccessToken(authResponse.access_token); })();Add Scriban support to VS Code
VS Code does not support Scriban by default. To enable syntax highlighting and autocompletion, install a Scriban extension. For Sitecore developers, the 'Sitecore Scriban Extension' is recommended as it provides autocompletion for Sitecore-specific methods and objects.Pass authentication from Apollo Gateway to subgraphs
Since the Apollo Gateway handles the initial request, authentication tokens (e.g., Firebase ID tokens) should be verified at the gateway level. The resulting user identity (likeuserId) is placed in the gateway's context and then forwarded to underlying services via HTTP headers using a customRemoteGraphQLDataSourceimplementation.// 1. Extract userId in Gateway context const server = new ApolloServer({ gateway, context: async ({ req }) => { const token = req.headers.authorization || ""; try { const user = await firebaseApp.auth().verifyIdToken(token); return { userId: user.user_id }; } catch (e) { return { userId: undefined }; } }, }); // 2. Forward userId to subgraphs via headers class AuthenticatedDataSource extends RemoteGraphQLDataSource { willSendRequest({ request, context }) { if (context.userId) { request.http.headers.set("userid", context.userId); } } }Eliminate render-blocking hydration scripts in Sitecore React
In many Sitecore React implementations, inline script tags used for hydration block the rendering of subsequent HTML. To fix this, wrap the hydration scripts in HTML comments and add a custom attribute (e.g.,data-react-hydrate) to identify them. Then, use a client-side script to find these commented-out blocks, extract the JavaScript, and execute them dynamically using theFunctionconstructor. This allows the main client bundle to be moved to the bottom of the page without breaking the hydration process.<!-- Example of commented out hydration script --> <!--<script data-react-hydrate> ReactDOM.hydrate(React.createElement(ClientX.default.Navigation, { ... })) </script>-->
<!-- Client-side execution logic --> <script> import "expose-loader?exposes=React!react"; import "expose-loader?exposes=ReactDOM!react-dom"; import "expose-loader?exposes=ClientX!../src/components";
const bodyHtml = document.body.innerHTML; const scriptTagExpression = /(?=<!--<script data-react-hydrate)([\s\S]*?)-->/gm; const scriptTagContentsExpression = /^<!--|-->$|<script data-react-hydrate>|</script>/g;
const matches = scriptTagExpression.exec(bodyHtml); matches?.forEach((match) => { const scriptTag = match.replace(scriptTagContentsExpression, "");
try { Function(scriptTag)(); } catch (e) { console.error("hydrating error:", e, scriptTag); } return ""; }); </script>
Configure GitHub Actions workflow for AzCopy Azure Function deployment
This workflow uses thekheiakiyama/install-azcopy-actionto install the AzCopy tool, cleans the remotewwwrootdirectory, and copies the build artifacts to the Azure storage account using a SAS token for authentication.name: Deploy Azure Function on: [workflow_dispatch] env: NODE_VERSION: "14.x" # Format: https://{storageaccount}.file.core.windows.net/{filesharename}/site/wwwroot AZUREFUNCDEST: "https://${pathtostoreageaccount}.file.core.windows.net/${filesharename}/site/wwwroot" jobs: build-and-deploy: runs-on: windows-latest steps: - name: "Checkout GitHub Action" uses: actions/checkout@main - name: Setup Node ${{ env.NODE_VERSION }} Environment uses: actions/setup-node@v1 with: node-version: ${{ env.NODE_VERSION }} - name: Install azcopy uses: kheiakiyama/install-azcopy-action@v1.0.3 - name: "Build application" shell: pwsh run: | npm run build - name: Clean remote directory uses: kheiakiyama/install-azcopy-action@v1 with: version: "v10" # Note: SECRETSASTOKEN is a GitHub Secret containing the SAS token - run: azcopy_v10 rm '${{ env.AZUREFUNCDEST }}?${{ secrets.SECRETSASTOKEN }}' --recursive - name: Copy files to Azure uses: kheiakiyama/install-azcopy-action@v1 with: version: "v10" - run: azcopy_v10 copy './packages/azure-functions/*' '${{ env.AZUREFUNCDEST }}?${{ secrets.SECRETSASTOKEN }}' --recursiveImplement Azure Blob Storage for Sitecore JSS Editing Data Cache on SWA
Azure Static Web Apps (SWA) does not support read/write access to the/tmpdirectory, causingEditingDataDiskCacheto fail with 500 errors in the Experience Editor. To fix this, replace the default disk cache with a custom implementation using Azure Blob Storage via the@azure/storage-blobpackage. This allows preview data to be persisted in a Storage Account container.import { BlobServiceClient, ContainerClient } from '@azure/storage-blob'; export class EditingDataBlobCache implements EditingDataCache { private containerClient: ContainerClient; constructor() { const blobServiceClient = BlobServiceClient.fromConnectionString( process.env.AZURE_STORAGE_CONNECTION_STRING ); this.containerClient = blobServiceClient.getContainerClient(process.env.AZURE_BLOB_CONTAINER); } async set(key: string, editingData: EditingData): Promise<void> { const blobName = key + '.txt'; const blockBlobClient = this.containerClient.getBlockBlobClient(blobName); const data = JSON.stringify(editingData); await blockBlobClient.upload(data, data.length); } // implement get method similarly }Apply a temporary JS fix to Azure DevOps using Fiddler AutoResponder
To resolve the
childFoldersruntime error in Azure DevOps without waiting for an official patch, you can use Fiddler to inject a modified version of the frontend JavaScript bundle:- Download the JavaScript bundle responsible for the error from the
cdn.vsassets.ioURL found in the network trace. - Beautify the code and locate the
createFoldersStructurefunction. - Replace the failing line
o[p.toLowerCase()].childFolders.push(c)with a ternary check:o[p.toLowerCase()] ? o[p.toLowerCase()].childFolders.push(c) : null. - Configure Fiddler's AutoResponder to intercept the request for the original JS bundle and serve the modified local file instead.
- Ensure Fiddler is configured to decrypt HTTPS traffic by enabling the 'Decrypt HTTPS traffic' checkbox in settings to allow the certificate injection necessary for intercepting CDN calls.
- Download the JavaScript bundle responsible for the error from the
Serve Next.js static assets via Azure Functions
Next.js static assets (e.g.,_next/static/...) are not automatically served by the custom server in an Azure Function environment. This is solved by creating a specific proxy to intercept static file requests and a corresponding Azure Function (serveStaticFile) to read the files from the.next/staticdirectory using the filesystem (fs) module.// proxies.json configuration { "proxies": { "staticFiles": { "disabled": false, "matchCondition": { "methods": [ "GET", "OPTIONS" ], "route": "_next/static/{*path}" }, "backendUri": "https://localhost/serveStaticFile?path={path}" } } }// serveStaticFile function import { Context, HttpRequest } from "@azure/functions"; const fs = require('fs').promises; const path = require("path"); const nextPath = path.dirname(require.resolve("../../basic-nextjs-example/.next/BUILD_ID")); module.exports = async function (context: Context, req: HttpRequest) { const staticFilePath = req?.query?.path; try { const data = await fs.readFile(path.join(nextPath, "static", staticFilePath)); return { body: data }; } catch { return { body: `File not found ${JSON.stringify(req.query)}` }; } }