Showing posts with label headless. Show all posts
Showing posts with label headless. Show all posts

Wednesday, 20 November 2024

Considerations for an OrderCloud (commerce) project

Unfortunately this year I didn't end up making it to Symposium, which was bitterly disappointing as it would have been fantastic to also attend the MVP Summit. Though I had prepared a presentation which was accepted by the Symposium team, it was designed to be (and required to be) co-presented with a client who ended up withdrawing. So while I didn't get to present (and I may hopefully yet at a SUG in the new year), I figured I would share the content in a blog post.

Undertaking any sort of enterprise site build is generally a mammoth effort, but when you factor commerce into the mix it can quickly become exponentially more complex, and risky - since there are now direct cost implications. Fortunately I'm here to report that if you keep things reasonably simple, and understand the tools you're working with - in our case, mainly Sitecore 10.2 (headless) and OrderCloud - it's all very much achievable. In our case, the build (for release 1) was around 8 months, which I personally feel is on the quick side.

Preparation

Documentation & Architecture

The first (and in my opinion most important) thing in any project is the preparation, and that means understanding your requirements in detail, and documenting them somewhere easily available to the full team (and any other partner teams who might end up working on the project). Make sure you have very good BAs involved, and I would highly recommend a wiki, or at least something which allows users to comment so that they can ask for clarification on any points that they don't understand, and the page can be updated with further information as it becomes available.  The documentation of business requirements should be as thorough as possible from the start, so that further planning and build (see sections below) can be based on it and prevent any (potentially major, time-consuming) changes if anything is missed.  It's very easy to say "we need the ability for users to apply coupon codes" without realising the deep deep rabbit hole promotions can very quickly become, for example.

Another key first step in any project is to understand (and potentially select) the tools with which you will be working; make sure they provide the capabilities necessitated by your business requirements (documented above); and put together a solution architecture so that everyone knows and understands how the information is flowing.  In our case, like most commerce builds, they key tools involved were:

  • Identity provider
  • PIM (source of truth for product info)
  • CMS (or DXP if you want to be fancy)
  • Middleware (optional)
  • Payment gateway
  • Order management
  • Downstream systems such as invoicing or analytics

In our case it was a bit of a rebuild, so the identity provider, PIM, and downstream systems were already in place; Sitecore 10.2 was already in use, so would also be used to enhance the product info; OrderCloud was selected for order management; and the client chose to go with a BFF (backend-for-frontend) pattern (using .NET 8) to support the frontend and extend the OrderCloud APIs.

It's worth pausing a second to reflect on the fact that there are both positives and negatives to having a BFF, particularly when you're using Next.JS for your "head" (which also provides server-side functionality). Personally (obviously heavily project-dependent) I think you can get away without one, but it can certainly come in handy.

  • Positive: It can be re-used between frontends, such as websites and mobile apps
  • Positive: It can be useful to have an extra layer to extend the OC APIs, merge product data with data from Sitecore, and handle exceptions
  • Positive: It can be used to house webhook endpoints called by OC (eg. on order submission)
  • Positive: It can be separately, and more rigorously, secured
  • Negative: It requires additional server(s) and infrastructure management
  • Negative: It's an extra layer, and one more thing to think about, when you already have "backend" Next.JS API endpoints available

OrderCloud

Another important consideration during preparation is around authentication, considering that OrderCloud provides SSO via OIDC.  In our case we were already using next-auth and another identity provider for other client sites, and SSO between the client sites was a requirement, so rather than rewrite all the client sites to use OC as the identity provider we opted to use OrderCloud impersonation in the BFF layer. It's great that OrderCloud provides flexibility by providing both these options.

Once you've documented your business requirements you will hopefully understand all the attributes and metadata that are associated with your products, and may recognise that you have a parent-child relationship between products. I have documented extensively the options available in OrderCloud around variants and parent-child products so I won't touch on it again here, however I'd highly recommend a read if you haven't already looked into it. Once you have a solid understand your product data I'd highly recommend creating a spreadsheet outlining: whether the data is read-only or can be modified by authors, what the expected values and lengths are, and whether the data is going to be stored in OrderCloud (ie. as Product xp data) or a separate system such as Sitecore. As you may be aware, Product xp has a max limit of 8000 characters, which may seem like a lot but can quickly and easily be used up and is not recommended for things such as rich text. My recommendation would be to use it only for data on which you will be searching / sorting / filtering products, and store the rest in Sitecore where you have almost unlimited flexibility (see build tips below on a note about sync'ing).  One key thing to note here is that if you want to give users the ability to sort products based on price, you will need to store the price in product xp data.  This is probably the biggest limitation I have come across in OrderCloud, but in our case it was easily surmountable as products had quite simple pricing, plus we had the added benefit of the BFF layer to house extra logic, so we were lucky this time. 

The final aspect of OrderCloud planning I'll touch on is the webhooks / integration events available. You should certainly familiarise yourself with the Order Checkout integration event, which is used during calculations (eg. tax), validation (eg. promotions), and of course order submission, so you probably won't be able to get away without implementing this one.  The other one which we found extremely handy is product synchronisation, which is fired whenever a product is created or modified, and can be handy to calculate and populate xp values (see paragraph above). This is fired for both parent and child products, and also when a product's price schedules are modified, so it's an extremely handy tool to have in your back pocket.

Project Planning

I'm no Project Manager, but it would be remiss of me to neglect to mention project planning in the preparation phase. Ensure you have your boards / user stories / tasks in whatever tool you use, and make sure your epics and sprints are defined, or you've at least understood the main phases of the project.  As I touched on in the foreword I would highly recommend launching an MVP (whatever that means for your project) and quickly following that up with a second release to flesh out the functionality, just to reduce complexity of the initial build, and keep timelines manageable.  

Key components of a commerce site you'll want to factor in: product listing page (PLP) including search / sorting / filtering, product details page (PDP), wishlist (optional), cart, checkout, confirmation, previous order listing, and of course all your standard web pages along with banners, carousels, rich text, and the like.

Build Considerations and Learnings

I feel at this stage (at least at the time of writing) Sitecore has gone all-in on Next.JS (before you comment: yes it seems this is likely to change in the near future), so depending on when you're reading this it's likely you've selected the Next.JS route, however it's still important to understand whether you're going SSR or SSG, what your ISR interval will be (how often are your products or content changing?), where your logic will live (Next.JS APIs?), and of course where your head is going to be hosted.

Additionally, authentication can be more complex than you expect, especially if you require SSO between other sites in the same ecosystem. As mentioned above, we went with the next-auth route (I'm keen to know what other options others may have gone with, let me know), but it's important to keep your auth and OC token in sync (if you're not authenticating through OC, at least).  Make sure users are logging out of both, anon carts potentially being deleted, and of course sessions and tokens are ending at the same time.

You may have content or products which should change or show/hide depending on your visitors' region, as we did. You can, of course, take advantage of the browser geolocation API, but if you happen to be running XP (as we were) you can also take advantage of Sitecore's geo-IP lookup - just don't forget to enable geo-IP in your Sitecore license.

If you're building a commerce site one of your primary considerations will likely be getting your products sold, so SEO should be front of mind. Of course you should aim for a fantastic pagespeed score, use the usual HTML and opengraph metadata, and have a sitemap, but also consider using microdata such as Product to enhance your page markup.  If you're migrating from a previous commerce implementation, or regularly removing products, you should also factor in setting up redirects from previous product URLs to your new product URLs, or just to PLP, as Google penalises 404 responses.

Tips and Tricks

  • Get used to working with an OC token (JWT) and using impersonation - your token is your context user (with specific permissions allowed), and even in the OC Portal you are actually using impersonation and have a token
  • OrderCloud xp properties are case-sensitive - OrderCloud will happily allow you to add price and Price which may not be very obvious when you go to deserialize/read it from your code
  • OrderCloud allows you to move users between Buyers, however you can’t move users with “open” orders (you must ensure they are "completed")
  • Make sure your BAs and developers are familiar with order of operations eg. promos are applied before tax is calculated
  • If you want to read Cart data (as opposed to Order data), there is a hidden (or at least not well documented role) that you will need: UnsubmittedOrderReader
  • Get used to writing a lot of loops: OC will only let you retrieve 100 items at a time
  • Facet values returned in the metadata are based on the first 50 results
  • Try to avoid using the .NET decimal type in xp - you might try to store 0.5m but end up seeing 0.5000001 stored in the xp
  • Webhook secrets have a 50 character limit
  • There is nothing stopping you from having a negative total, so make sure you cap your promotions
  • For guest/anonymous users, you cannot add items to a cart once it has been submitted. You must get a fresh token and create a new cart.
  • Don’t forget to assign your checkout integration event to your API client(s) otherwise you might end up wondering why it's not firing
  • For product facet values: take advantage of ProductSync events to create custom xp values which correspond with the facet values you need (eg. Price Range $100-$200)
  • Create publish event handler / publish webhook in Sitecore to sync data from Sitecore to OC, so that you can take advantage of in-built OrderCloud search/filtering features (if you're not using a third-party search tool)
  • Always validate your payment in the backend. This can be a good opportunity to patch your payment with Accepted=true.
  • Whenever you add a new cart line item, or patch the cart or a cart line item, you must call the order calculate method. Before submission you must also call order validate. Whenever you add a payment you must also (again) call order validate before you submit (ensuring that your payments with Accepted=true add up to the order total).

Testing

Performance should always be a key consideration throughout any project, and hopefully also being tested throughout, but there will likely be a specific load testing phase. Like any project, scale your servers appropriately (automatically if you can) and I would recommend notifying the OC team when you are planning to conduct your load tests so that they don't send concerned emails your way.  A key thing to note is that OrderCloud implements throttling in non-production environments (and don't publish any metrics around how throttling is implemented) and we certainly hit this during our testing. 

Depending on your code, you could implement their Throttler or just aim to reduce the requests you make to OC as much as possible. An example would be instead of using a loop in your code to GET individual products by ID, perform a single GET with ?ID=id1|id2|id3. We didn't end up needing to cache data but that is certainly an option depending on your load and the frequency with which your data changes. 

Finally, use a filter over a search where possible. Instead of ?searchOn=ID&search=id1 just use ?ID=id1. This applies to pretty much any entity.

Security is also hopefully also always front of mind by all architects and developers, but likely you will have a penetration testing phase of the project. Make sure that any security tests don't make it through to OC - ie. make sure that any user fields, query strings, or anything else that might end up in API calls to OrderCloud is sanitised. Sitecore/OrderCloud has a usage policy which prohibit testing of their services, and testing of your application may inadvertently appear as though you are testing OrderCloud.

Additional security considerations:

  • Make sure you are using a secret string for your webhooks (secret) / delivery configurations (secret) / integration events (hash key) which you are validating in the backend - note that some of these fields have a 50 character limit
  • You may also need to allow OrderCloud access to your webhooks through things like CloudFlare or WAFs.  If you are using CloudFlare, you may want to also additionally validate the webhook secret using a CloudFlare worker. After sending a request via email, the OrderCloud team kindly (and quickly) agreed to implement a user-agent which we found was previously causing the requests to be blocked by default WAF rules. Thanks so much guys!
  • OrderCloud is PCI compliant (assuming you don't go doing something silly like storing credit card data in xp)

Release

The release for every project will likely be different, so I won't dive into too much detail. Assuming you have ensured you have your environment variables are up to date with things like OrderCloud marketplace ID, API keys, webhook URLs, and secrets, most of your focus will be on other systems such as Sitecore or your "head".  Like any release, you will hopefully have a release plan outlining the various systems and order in which they will be deployed, and OrderCloud will likely be towards the beginning as it likely does not have many - if any - dependencies.

One thing I would highly recommend is having a script to create or update the data for your environment(s), whether they be non-production or production.  This could be in the form of a Node or .NET script, or a simple Postman collection that you run.  Note that entities need to be created in a certain order (eg. catalogs before categories before products) and that when creating a new environment from scratch you may have a couple of manual steps, such as copying the API keys (which have IDs generated on creation) to impersonation configs.

After Launch

Of course, things don't end after launch! It's important to sanity check not only your visitor-facing content (and check for any broken links) but also monitor how things are running behind the scenes.

Make sure you keep a close eye on, at least:

  • your server metrics, 
  • your logs,
  • analytics - your visitor count, and any purchase data,
  • your pagespeed score, 
  • your SEO ranking

It's not specific to a commerce implementation, but you should always ensure that you have sufficiently detailed logging to allow you to debug issues. Make sure you're not just logging that an error occurred, but who triggered it, what they were trying to do, and any other context which you may need to associate the debug log with an email or support ticket that customers may raise. When you're dealing with a commerce site you have the

Finally, ensure you scale your servers based on your metrics (or better yet, implement auto-scaling) to ensure you're not over-spending!

Conclusion

While undertaking an enterprise commerce site build can be a mammoth and daunting task, having a solid understanding of your requirements and tools, as well as keeping things simple for first launch, can go a long way to keeping things manageable (dare I say enjoyable?).  Sitecore has been around forever at this point, its capabilities are well known and documented, and they play very well when cooperating with OrderCloud (a relatively new player by comparison). While OrderCloud has a couple of limitations (which may affect you more than it did us) it is an easy to use, flexible, and performant solution, which should allow you to get up and running quickly and painlessly. If in doubt, reach out to Sitecore and see if you can get access to an OC Solution Engineer to assist with you on your journey.

This ended up being quite the essay, and I considered breaking this into multiple blog posts, but hopefully it's easy to skim and come back to later where required to refresh your memory. Hope you enjoyed the read and got something out of it. Good luck on your commerce journey!

Sunday, 8 October 2023

Security & Permissions with headless Sitecore

Security is a big headline these days (and should always be in the forefront of anyone's mind) and when it comes to content permission and user authorization the Sitecore "monolith" has always provided these capabilities out-of-the-box; however with the giant shift to headless over the last couple of years (or more) and now the move to SaaS these have grown vastly more complicated than the simple out-of-the-box content-permission-setting capabilities we used to know and love.

The issue

So why is it that things are now more complicated?
With the move to headless and composable, we have more of a separation of concerns - in this case, separation of the authentication/authorization, the display (/render) of content, and the provision of content.  This is most evident (technically, at least) in the use of REST and GraphQL by the headless code to retrieve content from the CMS (or DXP if you want to be fancy) where renderings were previously automatically provided their content by Sitecore.

The Sitecore "monolith" provides all 3 of these capabilities: 

  • Auth* through old ASP.NET users/roles, Federated Authentication, or Sitecore Identity 
    • Authenticates and assigns roles to user for authorization
  • Display/render of content through .NET MVC controller/view renderings (let's not talk about webforms or XSLT)
    • Authorizes user by checking permissions set on content in Sitecore XM
  • Provision of content through (amongst other things) datasources provided and consumed by the renderings 

In the new headless/composable world, these would be:

  • Auth though a separate identity provider (IDP) such as Okta / Auth0 / OneLogin / MS Entra
    • Authenticates and assigns roles to user for authorization
  • Display/render of content through headless SDKs such as Next.js (using REST / GraphQL)
  • Provision of content from Sitecore XM / Content Hub / Content Hub One

Notice something missing in the second list? Hopefully you did, as it's highlighted in bold in the first list!
There is now a disconnect between the roles assigned to the user by the IDP, and any roles you create in Sitecore / the permissions set on content.

This disconnect has no doubt been encountered by anyone working in a headless environment which requires their users to log in, and I have seen a few examples of how various people have thought about / tackled a solution. The following sections outline my approach.

A solution

There are 2 gaps mentioned above, and in case you missed them these are:
  1. A disconnect between roles assigned to a user by the IDP and roles in Sitecore (ie roles assigned to content)
  2. No out-of-the-box capabilities by the headless SDK to determine whether users are authorized to view content

You might be thinking "but both the GraphQL and REST endpoints support authorisation!". This was my initial thought, and I spent quite a bit of time deep diving into whether this was a viable option (ie. calling the layout service / GraphQL using the authenticated user details from the headless code). Let me save you a lot of time and headache: short of a quick and dirty option (simple logged in / not logged in user content, for example by swapping which API key you use to call the layout service) this isn't going to be an option you can use for anything serious.

I have seen other solutions which propose mapping the permissions (ie. roles <=> content) in a separate system, however my preference was to keep this within Sitecore. Sitecore XM still offers a flexible and robust ability to assign which content should be accessible by which users/roles, and building this again / finding a separate solution just seems like extra effort for no reason. I also personally do not feel like using this existing functionality goes against the spirit of composable at all. This mindset was the foundation for the remainder of the approach below.

I'm not going to dive too deeply in to the first point in the list above - suffice it to say there will need to be a way to ensure your roles in Sitecore match the roles assigned by your IDP. There are at least a couple of options:

  1. Configure role serialization, dynamically generate a yml file containing the roles, and dotnet sitecore ser push it
  2. Create an API endpoint on your Sitecore CM which calls System.Web.Security.Roles.CreateRole()

The meat of the dev work required, as far as I'm concerned, lies in exposing the roles and consuming them as part of authorization in the headless code.

Exposing the permissions

After setting the content (item) permissions (an out-of-the-box Sitecore XM exercise), the first thing that needs to be done is exposing these permissions. Content is consumed by the headless code either via the layout service, or GraphQL, so these are the 2 scenarios that need to be covered.

GraphQL: 

The quick and dirty way is to patch out the standard field filter, and add the security fields into your schema.
Note: this is not going to work if you're using Edge (/XM Cloud).

If you want a version which is compatible with Edge / XM Cloud, you'll want to create your own "faux security" field (you might call it "permissions"), and copy the standard (__Security) field value to this custom (permissions) field in a item:saved event handler. See this stackexchange answer for an example of a similar event handler.

Layout service: 

See sample repo

The layout service filters out all the standard fields (including __Security) in the FieldFilter method of JssItemSerializer (or whichever item serializer your site is using) so we will need to patch that out and add our own filter.
We then want to serialize our security field in a more readable format, which we can do in a custom SecurityFieldSerializer.

After this is done we can see all the __Security fields in our layout service result!

Authorization in Next.js

When it comes to Next.js, authentication really comes down to next-auth which has a host of IDP plugins out of the box (or you can develop your own easily enough if you really need / want to).  Authorization, however, is a custom exercise.  Long story short: once the SDK has called the layout service, the headless code needs to parse the permissions and prevent the user from seeing the page, or hide the appropriate content on the page from the user. 

Again, there are 2 parts to this: security at a page level (not authorized to view the page) or at a component level (hide a component from the user by removing it from the layout).

These can both be accomplished largely by customising normal-mode.ts (at least in Sitecore 10.2) with a helper to parse un-formatted security fields.

Page level (lines 68-74) and component-level (lines 77-80):

In the former we simply check the page security field, and in the latter we loop through all placeholders and check the security field set on the datasource.  This does not necessarily cover 100% of your use cases (components without datasources for example) but the rest can certainly be implemented in a similar fashion.

Conclusion

Securing certain content - to be shown only for authorized users - has been a site requirement for almost as long as the web has been around.  The advent of headless and/or composable mandates a new way of thinking and new approach to securing your site content.

With the logic provided above you should be able to both cover the vast majority of your permissions cases, as well as have a solid foundation for custom use cases where permissions might be needed within each of your components.

If you've made it this far - firstly thanks for reading! - I hope you learned something new today, or maybe re-enforced something you already knew. I'd love to get your feedback either way, so please leave a comment below, whether it's in agreement or some constructive criticism!


Tuesday, 22 August 2023

502s Massive Headers and So Many Cookies

We recently came across an (initially very confusing) issue where, on our cloud environments, a couple of fairly inconspicuous - but consistent - pages were making the server return a status code 502, or 520.  These environments had Cloudflare in front of them, which was showing us the 502 response, but our development environments (without Cloudflare) had no issues and the same pages and content loaded just fine.

Now I'll preface this by saying it was certainly something we (now know we) were doing incorrectly, but for the sake of anyone else who happens to come across the same thing I thought I'd share the debugging, findings, and fix.

So the natural first step to narrow down the cause of most things is to remove stuff from the page until it starts working again! By first removing components from the page(s), and then removing particuar portions/lines from those components, we found that it seemed to be next-auth related lines where we were fetching session info using useSession() (frontend) and/or getServerSession() (backend).  At this point we were pretty stumped: these same calls were working just find on some pages, but on a couple of particular pages they seemed to be causing issues... time for a web search to see if we can at least narrow things down!

A quick search for "502" and "next-auth" brings up a few useful results, but in particular this stackoverflow answer seemed to indicate it might be something to do with headers. At least we had somewhere to start! Hosting for this project was in AWS, with EC2 instances behind a load balancer, all of which sits behind Cloudflare.  As Cloudflare is the front endpoint showing the error, the natural instinct is to pit it on something to do with Cloudflare.  Hitting the endpoint from another server within the same network (bypassing Cloudflare) showed that the 502 was still returned. After that, the infra team provided us with the info we needed about header sizing (from the AWS load balancer limits):

Name Default Adjustable
Request line 16 K No
Single header 16 K No
Entire response header 32 K No
Entire request header 64 K No

It didn't take long to find that this was indeed likely the issue - a quick look at the header size for one of the pages showed it was certainly above the limit. Next question was obviously: "why"? That's quite a hefty size for headers. Again, taking a look at the headers for one of the pages, it didn't take long to see the issue:

and there was more to it than just what's shown here!

Wow, that's a lot of (very big) cookies being set in the headers... a very big WTF was in order!

Now for those who aren't familiar, these cookies are set by next-auth and split/chunked (session-token.0,  session-token.1 etc.) at 4kb by default. That's fair enough, we expected to see a couple and potentially very large ones; certainly not up to a dozen or so.

The Realisation

What came next was a lot of debugging, stepping through our code, the next-auth code, and to skip to the end: the realisation that every time getServerSession() is called, a new cookie (chunked, so actually multiple cookies) is set!

In our case, we had a few Sitecore headless components which had their own getServerSideProps() (component-level data fetching) and inside this method each component was calling getServerSession() to get the session data it needed to fetch its data. Ergo for each component using the session, a new cookie was created, blowing out the header size (and potentially causing session issues as well).

The Solution

So we had pinpointed the issue - now what? These components needed the session info (eg. user's account ID) in order to fetch their data, this wasn't just something we could do without. 

The key here is to understand how the component-level data fetching works: if you take a look at the [[...path]].tsx file which you get from a default instantiation of a Sitecore headless project, you will see inside it a getServerSideProps() function, and inside that a call to sitecorePagePropsFactory.create(). This is the starting point for the getServerSideProps() call of each component. 

If you follow the code, you will see the SitecorePagePropsFactory in page-props-factory\index.ts loops through each "plugin" and executes it, using a context, and setting props.  This context is the one which comes through in the getServerSideProps() function at the component level.

In [[...path]].tsx:

Using this knowledge, we can fetch our session in [[...path]].tsx, the starting point for all server-side logic, and pass it through in the "plugin context" to each component, so that we don't have to re-fetch it.

const session = await getServerSession(context.req, context.res, authOptions);
await sitecorePagePropsFactory.create({ session, ...context});

then in each component:

export const getServerSideProps = async (_rendering, _layoutData, context) {
  const fetched = fetchSomeData(context.session.userId);
}

Hope this helps anyone else who comes across this issue (or something like it)!

Sunday, 23 July 2023

Secure (HTTPS) Your Non-Prod Next.JS Headless Sites

Preface: this isn't something you'll want to want to use in production, but great for non-prod or local environments!

If you're running Sitecore on Windows servers (not Azure PaaS where you can simply get a free managed certificate), and want to re-use your infrastructure (eg. in a non-prod environment or for testing purposes) you may choose to use that same Windows machine to run your headless app. This can save on costs, energy that can be spent on development (or setting up production), and help just to keep things simple.
Obviously there are heaps of hosting providers such as Vercel where you could run it, and if that's an option that works for you I would certainly recommend it. If not, read on...

The simplest way would be to copy your code to the same server - in a new folder beside your Sitecore installation - install Node and run: npm start:production. This will start your app on port 3000 (or whatever port you have configured).

This is fine if you just want to preview your app, but if you want it to work inside of Experience Editor, and your CM instance is using https, you'll need your app to also be served securely (or else you'll see a very broken site!).

Obtaining a Domain Name

I'm not going to tell you how to go about getting a domain name. Presumably if you've got Sitecore up and running somewhere you know how to go about getting a domain name, or already have one (and/or can use a sub-domain). I'm a fan of Namecheap, but whatever is cheapest/easiest!

Obtaining a Free Certificate

Easiest way to get a free certificate (on pretty much any system) these days? LetsEncrypt!

  1. Download CertBot from the letsencrypt site
  2. Install it
  3. Temporarily stop your IIS server (if it's running) so that Certbot can use its own server
  4. Request a certificate by running:
    certbot certonly --standalone -d "*.your.domain.com"
    (or any of the other methods such as DNS validation)
  5. Start your IIS (if you stopped it in #3 above)

HTTPS Using the Certificate

Since we're using Node anyway to run the headless app, we can make use of the local-ssl-proxy package to proxy your app through HTTPS with a single line!.

  1. Open a command prompt and run:
    npx local-ssl-proxy --key C:\Certbot\archive\your.domain.com\privkey1.pem --cert C:\Certbot\archive\your.domain.com\cert1.pem --source 443 --target 3000
    (obviously substitute your port 443 for anything else if it's occupied!)

Simple as that! It's so easy there's no excuse not to be using HTTPS for non-prods (or even local!) :)

Wednesday, 10 May 2023

Locking down requests from Sitecore to Head

For those coming from on-prem or PaaS the leap to the exciting new world of Sitecore SaaS can be quite daunting, with many new factors and considerations - not the least of which is security (which should always be front-of-mind).  Your dev teams may be busy brushing up on containers and headless, but infra teams will be more concerned with integrating with XM Cloud, most notably with the Head.

Most of you will (hopefully) already be familiar with a couple of the ways that communication between the Head and Sitecore can be secured:

  1. A Sitecore API key - allows you to lock down your API calls to the Layout Service and GraphQL endpoints, as well as impersonate users.
  2. The JSS Editing Secret - used as a shared key to ensure the app and Sitecore Editor are the only parties authorized to talk to one another while in Experience Editor mode.

But what if you want a bit of extra security? Say you have a WAF (something to that effect) which only allows certain headers? As always, Sitecore is nice and extensible:

The class making the HTTP call to your Head app is Sitecore.JavaScriptServices.ViewEngine.Http.RenderEngine and it creates a HttpClient to make the requests using Sitecore.JavaScriptServices.ViewEngine.Http.HttpClientFactory which has the following method:

public IHttpClient Create(HttpRenderEngineOptions options)
{
	Assert.IsNotNull(options, "options");
	return new TimeoutCapableWebClient(options.RequestTimeoutMs)
	{
		Encoding = Encoding.UTF8,
		Headers = { [HttpRequestHeader.ContentType] = "application/json" }
	};
}

This HttpClientFactory is created through DI, so you can just inject your own and add your own headers! Too easy!

Thursday, 4 May 2023

Sitecore Headless Forms - Hidden Field Using Query Value

This one goes out to @AJS on Sitecore slack who asked how they could implement the query string provider in Sitecore Forms (headless/JSS).

Firstly, make sure you add the QueryStringProvider from Sitecore Forms Extensions, as well adding the required Sitecore item(s) to your instance.

Once that's done see the Sitecore documentation on adding a React/Next.JS form to your headless app.

Finally you're ready to implement a custom field factory (see my previous post) with a custom hidden element, setting the value based on the value from the query string:

Enjoy!

Friday, 28 April 2023

Sitecore JSS/Headless ColumnSplitter in 10.2 (with bonus Tailwind styles)

I saw a post in the Sitecore Slack channel recently asking about ColumnSplitter support in 10.2. This was something I have back-ported recently (as columns really are an essential component in all designs these days) and have been meaning to post about, so it was a good kick to get this post going. Thanks @gatogordo!

You may think that the ColumnSplitter component provided in the XM Cloud starter kit looks fairly straightforward and should work in older versions of Sitecore, but there are a few key changes that are required, both in the JS and in Sitecore, which I will go through now.

Note: I'm going to be using SXA and Tailwind in this post, to get up and running as quickly and painlessly as possible. No doubt this would work without them, but there would be additional / different steps involved.

TL;DR checkout the repo: https://github.com/moo2u2/jss-column-splitter-example


My repo includes a storybook demo

This is what we're going for!


First, ensure you have a 10.2 instance of Sitecore with (the correct version of) SXA and Headless modules installed. If you want to restore the items from the repo above, you'll also need Sitecore Management Services (for serialization) installed.

We'll start by creating a headless Tenant and Site in Sitecore, as well as creating a new Next.JS project with the same name as the site, and the correct version for Sitecore 10.2.

npx create-sitecore-jss@ver20 nextjs --appName jss-column-splitter-example

At this point it's worth running a jss setup to configure your Sitecore connection, and verifying that your app runs correctly in connected mode, because we're about to get into the fun part and add/change a bunch of stuff.

We'll start by creating a ColumnSplitter rendering, which I originally grabbed from the XM Cloud Starter repo (or you can just grab my back-ported version).  Next you'll want to add this to your placeholder settings (in my case, jss-main) so that you can add it to the page in Experience Editor.

then go ahead and add the component to your page (in that placeholder), either in Content Editor or Experience Editor.  

At this point it's probably worth taking a pause to explain how the column splitter works: 
Obviously (by default) it displays no content, so all the settings live in the rendering parameters (no datasource).
The rendering parameters are made up of 2 main parts: the grid, and styles. Both contain CSS classes, however they come from different parts of the content tree.

There are grid + styles options for each column, up to 8 columns (out of the box), as well as the styles for the component itself.

Rendering parameters live at: /sitecore/templates/Feature/JSS Experience Accelerator/Page Structure/Rendering Parameters/ColumnSplitter or {181740EA-A7AE-4799-A649-A75917570E38} and references a couple of other Templates you may not have: Base Rendering Parameters ({4247AAD4-EBDE-4994-998F-E067A51B1FE4}) and _PerSiteStandardValues ({44A022DB-56D3-419A-B43B-E27E4D8E9C41}).

If you open the rendering parameters at the monent, you'll likely notice that they are not displaying correctly:



Starting with styles: as with regular SXA, we need to create a "Styles" item under your Tenant > Site > Presentation, of type: /sitecore/templates/Foundation/Experience Accelerator/Presentation/Styles - {C6DC7393-15BB-4CD7-B798-AB63E77EBAC4}. Under that you can add your styles (I've added various backgrounds for demo purposes).


Now we can see the styles listed in our rendering parameters!


The grid parameters are slightly more complex, as they depend on the SXA grid system, so if you aren't familiar with it I would check out the documentation on how to Create a custom grid.

Because Bootstrap is old school and we're using headless, let's create a faux "grid" for Tailwind! I've added a few different widths for our columns:

Next we need to update our site settings to have the field for, and reference, this grid:
In /sitecore/templates/Foundation/JSS Experience Accelerator/Multisite/JSS Settings - {EC848505-D30C-4BDC-A0AA-7CC9D320085E} add /sitecore/templates/Foundation/Experience Accelerator/Grid/_Grid Mapping - {9D81C61A-0341-4312-816D-E5204385EA3C} to the inherited templates



Back up in our site settings we should see Bootstrap 5 selected as the default grid, which we can now change to our Tailwind grid!

This is the default - change it to our new Tailwind grid

At this point our rendering parameters should all be good and useable!


Finally we need to add our placeholder settings for each column. This is one of the key differences in back-porting it, as we don't use any wildcard asterisk. Just a plain old placeholder name.


With your ColumnSplitter added to the page, select a size (and optionally a style) for a couple of columns and let's take a look at the layout service to double check it's all good for the app to consume: 
/sitecore/api/layout/render/default?item=/&sc_apikey={YOUR_KEY}

We can see the component added to the placeholder, and values in the grid and styles but those aren't useable values...


This is where I introduce you to my mate Andy's blog post on Serializing rendering parameters in the layout service. 
Note: If you have selected multiple grid sizes (for different devices), you will end up with multiple pipe-separated GUIDs, which his blog doesn't cover. You will need to update your code to cater for this.
With the fix in place, you should see the rendering parameters serialized in JSON format:


We're now in a state where we're good to consume this from the headless app we created at the start! 
Run your app in connected mode, and input the following into your site settings:

Server side rendering enginehttp
Server side rendering engine endpoint URLhttp://localhost:3000/api/editing/render
ServerSideRenderingEngineApplicationUrlhttp://localhost:3000

and you should be able to see your component in the Experience Editor!

You can have a play around with different grid sizes and styles, and experiment with adding different content to your placeholders.

Extra Tidying 

Since we're using SXA, let's make the "Add randering" popup display the SXA way:
Under your Tenant > Site > Presentation, add an "Available Renderings" item of type  /sitecore/templates/Foundation/Experience Accelerator/Presentation/Available Renderings/Available Renderings Folder - {39A9ED72-407D-4440-BA50-30956834794E} and add your renderings.

This should make the modal popup much more user-friendly!



Wednesday, 22 March 2023

Custom Form Elements in Headless (JSS) Forms

I've been getting pretty deep into the Sitecore React/Next.JS Forms libraries over the last couple of weeks, and while there's some half decent information out there if you look hard enough, it's generally not that accessible or complete. For example, one of the better pages altering field types is hidden away in GitHub and doesn't seem to pop up for me in a Google search.

I find that Sitecore Forms, while a great form builder, still lacks (at least) a couple of fields which I would consider a must-have: hidden fields, and raw HTML. Fortunately these can be found in Sitecore Forms Extensions, so we can copy the relevant code from there.

Once you've got the fields into Sitecore - in the form builder and rendering their properties into the layout service - the next step is to ensure your React/Next.JS code knows how to render the fields. Fortunately per the link above, this is fairly easy to inject/extend using a custom field factory:

We can then add our custom field factory to our form, and we should be good to go!

<Form
  form={fields}
  sitecoreApiHost={process.env.SITECORE_API_HOST}
  sitecoreApiKey={process.env.SITECORE_API_KEY}
  onRedirect={(url: string) => router.push(url)}
  fieldFactory={CustomFieldFactory}
/>

Issues / wish list:

  • CustomFieldFactory.setComponent first parameter relies on the FieldTypes enum (but works with any string). This causes Typescript errors. It would be great if this parameter/enum were extendable.
  • The Form component extends Component<FormProps, FormState & FieldStateCollection> but is itself not generic! This means that props can't be overridden, which means adding custom properties causes Typescript errors (though the code still works). Ideally Form would be generic so that we could pass custom props and properly override the Form class.
  • Conditions are not included anywhere in the Sitecore Forms React code :( See an upcoming post for how we can achieve conditional logic using OOTB Sitecore Forms conditions.
  • There is no validation on form submission! This was very surprising (and frustrating) to find (and implement).
  • There is no validation for mandatory fields on a couple of fields, eg. CheckboxList
  • Fields are still submitted by the JSS code even when adding a 'disabled' attribute to the field. This super painful to work around!