Welcome to SUGNL

We organize three community events per year to bring together developers, architects, and tech enthusiasts. Join us for evenings of knowledge sharing, networking, and exploring the latest technologies.

Upcoming Event

OCT282026

๐Ÿ•’ 17:00 - 21:30

Wednesday


iO Den Bosch

Magistratenlaan 4

5223 MD โ€™s-Hertogenbosch


Coming by car?

Park at the parking garage

Vijverlaan 1a

5223 AW โ€™s-Hertogenbosch

SUGNL Meetup

An evening with SUGNL

Join us on 28 October for another Sitecore User Group Netherlands evening at iO in Den Bosch!

Weโ€™ll kick things off with food, drinks and time to catch up with fellow Sitecore enthusiasts, followed by sessions on AI agents, agent-ready rendering and a hands-on workshop where you can experiment with your own headless frontend.

As always, expect an informal evening with practical knowledge, good discussions and plenty of opportunity to meet others from the Sitecore community.

๐ŸŽค Inspiring talks๐Ÿ’ป Hands-on workshop๐Ÿฅ‚ Food, drinks and networking

From our community blogs

Sharp ideas, practical stories, and lessons learned straight from fellow builders in the SUGNL network.

Explore all posts โ†’

Ronald van der Plas ยท Oct 8, 2026

Building Agent rendering in Next.js and Sitecore

We've now come to the third and final part of our agent rendering series. In the first blog post, I wrote about the modern DXP frontends and how it's not always easy for AI agents to consume our content. In the second part, we've taken a look at the architecture that we need for this. In this final part, I've built a Proof of Concept to show how it might work. For the Proof of Concept, I used the Sitecore Next.js Skate park starter, this is a starter that is provided by Sitecore to help you get up-and-running for a headless frontend against SitecoreAI. In this starter, I implemented two separate capabilities: a consumer mode that allows components to render a flattened, non-interactive representation for AI agents, and HTTP content negotiation so the page can be requested as Markdown. You can find the implementation in my repo kit-nextjs-skate-park example on GitHub. The implementation deliberately keeps the normal human experience untouched. The last thing you want to do, is ruin your current site and the outstanding ranking that it might have. Therefore, a regular browser will still get the same rendering of the components as before. An AI agent can get a different representation of those same components, with interaction removed and hidden content exposed. When that agent asks for text/markdown, the page will be rendered first through the agent-friendly path before being converted to Markdown. One pipeline, two rendering outputs When first thinking about a proper solution to fix this. I thought about creating a separate pipeline to support the Markdown rendering. That would have created a whole new, separate pipeline to maintain. The first problem is not HTML versus Markdown. The first problem is that some components are designed around human interaction. A navigation menu may require a click, a carousel may only show one slide at a time, another component may even fetch additional information after it has rendered. Simply converting the initial HTML to Markdown does not solve any of that. So I ended up with the following concerns: Consumer: default | agent, Representation: HTML | Markdown The consumer mode decides what the component should render. Representation decides how the result will be delivered. When you look at the code, you will find that the consumer detection lives in consumer-mode.ts, and the AI-specific User-Agents are stored in ai-user-agents.ts. This is how the flow is implemented: The distinction between consumer and representation turns out to be important. The agent version of a component is allowed to have a different DOM structure, but it must remain the content equivalent to the human version. A carousel can become a simple stack of slides, a hamburger navigation will become an expanded list. If a component loads information through client-side calls, its agent representation must retrieve that information server-side instead. Allowing the agent to have all the relevant information it needs to provide good possible context for constructing an answer. Below here, you can see the flow how it goes: By using this flow, we keep the logical in the rendering component for agent or humans, allowing for an easier overhaul to output Markdown rather than HTML. By doing so, we only need to have one function to convert all components into markdown, rather than having each component create it's own markdown representation. Of course you could also do it like that, but I think you might end up in a maintenaince nightmare real soon. Detecting the consumer In order to create an implementation for detecting which type of consumer was accessing our site, I needed to understand how we could identify each one. For this, I've created a list of AI agents types and stored them in ai-user-agents.ts. The list is kept deliberately small and explicit, I don't want to change the flow for none AI user agents, that might cause an unwanted side effect. export const AI_AGENT_USER_AGENTS = [ 'GPTBot', 'ChatGPT-User', 'ClaudeBot', 'Claude-Web', 'anthropic-ai', 'PerplexityBot', 'Google-Extended', 'FacebookBot', 'cohere-ai', ] as const; A deliberate decision I took, was to keep the classic search-engine crawlers out of this list. Agent mode is not meant to become a generic "bot mode" for now. Search crawlers are becoming more able to work with Javascript actions as well, I don't want to risk harming your SEO ranking in the process. Let's move on, on how I implemented the user-agent decetion in consumer-mode.ts. This code turns the User-Agent detection into a typed value: export type ConsumerMode = 'default' | 'agent'; export interface ConsumerContext { mode: ConsumerMode; } export const detectConsumerMode = (headers: Headers): ConsumerContext => { const userAgent = headers.get('user-agent') ?? ''; return isAiAgentUserAgent(userAgent) ? { mode: 'agent' } : { mode: 'default' }; }; From this point on, we can start creating the implementation into the components. Adding consumer mode in Sitecore components To determine the type of consumer requesting the content, I wanted to avoid introducing a separate React Context provider purely for agent rendering. Every Sitecore component already has access to the current page through useSitecore(), so it made more sense to extend the existing page context with the consumer information. I therefore introduced a ConsumerContext type and extended the Sitecore page object with a consumer property. There was, however, a TypeScript issue I encountered with this approach. The SDK's Page is a type alias rather than an interface, so declaration merging was not an option. The solution was to define a local page type that extends the SDK type and becomes the application's source of truth: import { Page as SdkPage } from '@sitecore-content-sdk/nextjs'; import { ConsumerContext } from 'lib/consumer/consumer-mode'; You can see that in src/lib/component-props/index.ts. The actual page route then detects the consumer and adds the result to the page before it is passed into the existing Sitecore providers: const headers = await nextHeaders(); const pageWithConsumer: Page = { ...page, consumer: detectConsumerMode(headers), }; The wiring lives in src/app/[site]/[locale]/[[...path]]/page.tsx. From that point onwards, any component can make one simple decision: if (getConsumerMode(page) === 'agent') { // render the agent representation } Flattening interactive components For the PoC, I implemented the pattern in two components: Navigation and Carousel. For the navigation, the default experience exposes the interactive navigation behaviour, including a hamburger-style interaction for smaller screens. That is useful for a person, but not needed for an agent that doesn't need the UI experience. So when in agent mode, Navigation.tsx renders an expanded recursive list instead. The underlying content does not change; the links are all the same, we only removed the experience around it. In the Carousel.tsx component, we did more or less the same thing. The component uses an interactive carousel with previous and next controls. For a human, that is fine, for an agent, hiding the two other slides behind a next button makes little sense. So when run in the agent mode, the code simply stacks every slide in document order: const CarouselAgent = ({ slides }: { slides: CarouselSlide[] }): JSX.Element => ( {slides.map((slide) => ( ))} ); You might think, but is that really a huge improvement? Perhaps not really, I would have been better to use an example that also uses client-side loading, but you get the idea: don't create an alternate version of every component by default. Most components are probably already fine. Only introduce an agent representation where interaction, client-side loading or presentation hides meaningful information. The most important rule is content parity; ensure that every content remains in both representations. Rendering marking to the consumer Now that we've flattened our components for agent mode, we can continue one step further and render the content out as Markdown. Making our content even better to read for agents. For this to work, I didn't want to expose a new URL structure; you should simply be able to request a page, where, based on the Accept header, the rendering shifts from HTML to Markdown. In order to do this, I've altered the next.config.ts to use a header-based rewrite. { source: '/:path*', destination: '/api/agent-markdown/:path*', locale: false, has: [ { type: 'header', key: 'accept', value: '(.*)text/markdown(.*)', }, ], } The caller still requests:/Carousel, but can ask for another representation by: Accept: text/markdown. That request is internally routed to src/app/api/agent-markdown/[[...path]]/route.ts, therefore ensuring that the public URL never changes, based on the Accept header. Why render HTML first? Like I said before, I wanted to create a simple implementation that would work independently of the components. One might argue that it might be simpler to create a separate rendering that would fetch the raw Sitecore layout data and convert fields directly to Markdown. I deliberately did not want that, because of component behaviour. A component can do more than render the fields Sitecore originally gave it. It can resolve references, call another service, enrich data server-side, or replace client-side fetching with a server-side equivalent in agent mode. If Markdown starts from raw layout data, all of that is bypassed and needs to be rebuilt again. Or become highly intertwined with the default mode, waiting for a nightmare to happen. Instead, the Markdown route asks the application to render the same page again as HTML. The helper for that lives in render-agent-page.ts. It forwards only the request context that matters: copyHeader(request, headers, 'user-agent'); copyHeader(request, headers, 'accept-language'); copyHeader(request, headers, 'cookie'); copyHeader(request, headers, 'authorization'); headers.set('accept', 'text/html,application/xhtml+xml'); headers.set('x-agent-markdown-render', '1'); The original User-Agent is particularly important. If the original request came from GPTBot, the internal HTML render also comes from GPTBot. That means the normal page pipeline detects agent mode again, so the Navigation and Carousel components automatically render their flattened versions, thus making our life simpler with one implementation to maintain here. The Markdown implementation therefore inherits the agent rendering for free. GET /Carousel Accept: text/markdown User-Agent: GPTBot โ†“ Markdown rewrite โ†“ Internal HTML render User-Agent: GPTBot Accept: text/html โ†“ Agent-aware components flatten themselves โ†“ Flattened HTML โ†“ Markdown conversion The x-agent-markdown-render header is used as a recursion guard. If you've ever written recursive code, you might learned that hard way that it needs a stopping point... The internal request hits the same origin and pathname, so there needs to be a way to distinguish the internal HTML render from the original Markdown request. This approach does have a cost: one Markdown request results in an additional internal HTTP round trip. For a production implementation, that deserves measurement and probably optimisation, for example through caching. For this proof of concept, I preferred cleanliness and simplicity to understand the concept. Turning the rendered page into Markdown Now that we've the flattened HTML from our site, the final step is straightforward. We've built code to do the conversion in html-to-markdown.ts. Here we use Turndown together with the GFM plugin: const turndown = new TurndownService({ headingStyle: 'atx', bulletListMarker: '-', codeBlockStyle: 'fenced', emDelimiter: '*', strongDelimiter: '**', }); turndown.use(gfm); In order to further strip down the HTML, we look for Scripts, styles, templates, SVG markup and other irrelevant elements, which are removed before the HTML is converted to: Content-Type: text/markdown; charset=utf-8 There was only one minor problem when using Turndown. Removing the head section wasn't reliable enough in Node. The Title and metadata content could still end up in the Markdown output. So to guarantee a proper output, we strip the head block before Turndown sees it and keep the Turndown removal rules as a second line of defence. That is not the kind of thing that appears in the architecture diagram, but it is exactly the kind of thing you discover once you actually build the feature. Testing the solution The tests focus less on what the markup looks like and more on whether the contract still holds. consumer-mode.test.ts checks that the known AI User-Agents activate agent mode, while normal browsers and classic search crawlers do not. Next to that, the tests in Navigation.agent-mode.test.tsx checks that agent mode exposes the same links as the default navigation. Ensuring we don't break our existing SEO while outputting a different representation. Same content, different DOM shape. The point is not to create a stripped-down page that happens to be easier for a crawler. The point is to expose the same meaningful content without requiring human interaction to reach it. What to do before going to production? This is a proof of concept, nothing more. You should not put this into production, before making changes to performance and such. Also, ensure that you have at least a baseline of your site. That might include performance SEO, ranking and more. Once yoy have that in order, there are a few things I would want to improve before releasing into production. The first item on the list is caching. The Markdown response currently uses private, no-store. That is the safest starting point when request-time context, personalisation, or enrichment might affect the output, but it is not necessarily where I would leave it. A real implementation needs an explicit caching and revalidation strategy. Another thing to keep an eye out is personalisation, or A/B testing. What version are you going to show to an AI bot? Or perhaps even leave out the whole thing. It's up to you to decide. The next item is the internal HTTP round trip. It is a very convenient way to reuse the complete Next.js and Sitecore rendering pipeline, but it adds latency and work. If this became a heavily used capability, I would investigate extracting the agent representation and server-side enrichment into shared server functions so HTML and Markdown can serialise the same enriched model without requesting the application over HTTP. I would also make the consumer detection more configurable. User-Agent detection is a useful starting point, but the architecture should not depend on it forever. A future version could allow explicit agent-mode negotiation through a request header as well. Also, move the configuration to the configuration, rather than hardcoding it into the solution. Finally, this implementation currently lives only in the skate park starter. I deliberately kept the scope small, so I could prove the pattern before trying to generalise it across other starters. Source code The complete source code is available here: github.com/rvdplas/agent-rendering/tree/main/examples/kit-nextjs-skate-park. I mentioned earlier, this code is Proof of Concept. It is at best a reference implementation of the idea, including the trade-offs and some of the rough edges that came with building it. Where this leaves the idea When I started this series, the question was whether modern DXP frontends should start treating AI agents as another type of consumer. After building the proof of concept, I think the architecture is less exotic than it first sounds. The CMS does not need to change. The structured content does not need to be duplicated. The human experience does not need to be compromised. What changes is the assumption that every consumer needs the same representation. For a human, an interactive carousel makes sense. For an agent, a list of the same slides may be better. For a browser, HTML is the obvious representation. For an agent, Markdown may be easier to consume. The content can remain the same while the rendering layer becomes more aware of who, or what, is asking for it. That is the part of this experiment I find most useful. Agent-ready rendering does not need to become a separate delivery platform. In a modern headless architecture, it can simply become another capability of the frontend. With that said, it's a wrap on agent-ready rendering. Thank you for reading!

Read post

Ronald van der Plas ยท Oct 1, 2026

SitecoreAI is changing PipelineArgs serialization. What to do?

A small serialisation change with a bigger meaning Sitecore recently announced a change to how the PipelineArgs are serialised and deserialised in SitecoreAI. The Newtonsoft.Json configuration that currently uses TypeNameHandling.All will move to TypeNameHandling.None. For most implementations, this will probably go unnoticed, especially if you haven't deployed custom pipelines. Primitive values such as strings, numbers and booleans will continue to behave as expected. The main difference is that .NET type names are no longer serialised. The interesting part begins when custom code stores complex .NET objects in PipelineArgs.CustomData and later expects those objects to survive a serialisation and deserialisation round-trip. That pattern can depend on type information being written into the JSON itself. Once Sitecore removes that type metadata, the object may no longer come back as the same concrete .NET type. At first glance, the natural reaction is to treat this as a compatibility problem. Find the affected code, adjust the serialisation logic, test the result and move on. Technically, that is a valid response. Architecturally, however, I think it misses the more important point. If this change breaks custom in-process code in a SitecoreAI implementation, the first question should not necessarily be how to make that code work again. The first question should be why that code is there in the first place. That distinction matters because SitecoreAI is not simply Sitecore XP hosted somewhere else. The platform moved to a different hosting model, where custom business logic should increasingly live outside the managed runtime and integrate through supported APIs, webhooks and services. A breaking serialisation change can therefore be more than an upgrade task. It can reveal an architectural dependency that should probably have been removed already. How to test the new behaviour Sitecore provides a feature flag that allows you to enable the new behaviour before it becomes the default. You do this through a normal Sitecore configuration patch: true Deploy that configuration to a non-production environment and exercise the parts of the solution that contain custom pipeline logic, especially anything that stores complex objects in PipelineArgs.CustomData and expects those objects to be reconstructed after serialisation. If nothing breaks, you are probably done. If something does break, Sitecore's guidance is to update the affected customisation so it no longer relies on embedded type metadata, for example by using strongly typed models or explicitly reconstructing the expected object type after deserialisation. That is the short-term compatibility path, and it is useful because it gives customers time to find issues before the new base image becomes the default. For the full rollout guidance and configuration details, refer to Sitecore's original advisory and documentation. My conclusion: stop deploying your own code to SitecoreAI This is the part I think matters more than the serializer change itself. If enabling this flag exposes a custom pipeline dependency, I would not automatically treat that as a signal to modernise the pipeline code. I would treat it as a signal to remove the dependency. SitecoreAI is a managed SaaS platform. The further your implementation moves away from custom code running inside that platform, the better. Custom business logic should live in services you own and control, and SitecoreAI should be accessed through supported contracts such as APIs, webhooks and application extensions. That separation is important for more than just this specific change. Every piece of code deployed into the Sitecore runtime increases your dependency on internal implementation details. Today it is TypeNameHandling. Tomorrow it might be another framework change, another runtime constraint or another security hardening measure. If your customisation reacts to an event, use a webhook. If it needs to read or modify content, use the SitecoreAI APIs. If it performs business logic, move that logic into an external service. If it provides a larger extension, build it as an application instead of embedding it in the platform runtime. Of course, there will be older implementations where that migration cannot happen immediately. In those cases, fixing the serialisation logic may be a perfectly reasonable temporary measure. But it should remain temporary. Please advice the team to log this kind of technical debt, put in on your backlog. Do not invest more than necessary in keeping legacy in-process customisations alive. Use the feature flag to find them. Put them on the board. Fix them if you need time. Then remove them.

Read post

Ronald van der Plas ยท Sep 30, 2026

Architecting for Agent Rendering

In the previous blog post, I looked at a question that started with Scrunch AXP: if AI agents are becoming another consumer of our digital experiences, should we keep transforming websites afterwards, or should we start designing our frontends with those consumers in mind from the beginning? My conclusion was fairly simple. We should not create a second website for AI agents, and we should not create a second editorial process either. The CMS should remain the single source of truth. The content stays the same, but the representation will change depending on the consumer. For a human visitor, that representation might contain navigation, carousels, tabs, personalisation, and all the other things we have spent years building into modern digital experiences. An AI agent doesn't need many of those interactions. It might benefit more from a stable, semantic and complete representation of the same information. I called the capability agent rendering. The next question is: what architecture do we need to accomplish this? We are not building two websites This is the most important constraint. If every component suddenly gets a completely separate "human version" and "agent version", editors start writing different content for agents, and every new feature needs to be implemented twice. This will surely mean the architecture has already failed. The outcome should be much simpler. The CMS provides structured content, the frontend understands that content, and the rendering layer optimises it for a particular consumer. Conceptually: CMS โ†’ component data โ†’ rendering layer โ†’ representation The representation comes after the content. We are not creating another content model specifically for AI agents. Separate detection from rendering One of the first decisions is where we determine what kind of consumer is making the request. A CDN or edge layer could inspect it, middleware could do it, or the application itself could decide which representation to return. For existing enterprise websites, doing this at the edge can make a lot of sense. That is part of what makes an approach such as Scrunch AXP interesting: you can introduce agent-friendly delivery without rebuilding the systems behind it. For a new headless implementation, I would argue that we might want to take matters into our own hands. But I would not tightly couple the rendering architecture to bot detection. User agents change, new agents appear, and not every agent will identify itself in the same way. One part of the architecture should therefore determine which representation is requested, while another part knows how to render that representation. The rendering logic shouldn't care whether that decision came from a CDN, middleware, a request header or something else. The component data stays the same This is where headless architectures give us an advantage. By the time a frontend receives content from a CMS such as Sitecore or Storyblok, it already knows what that content represents. It knows that something is a hero, a product list, an accordion or a comparison component. That knowledge exists before the frontend turns the data into the visual experience we see in the browser. Take an accordion, for example: the CMS might provide five items, each containing a heading and some content. For a human visitor, we render those items as an accordion because it makes the page easier to scan. But the underlying information isn't really an accordion. It is five sections of related content. An agent renderer could simply expose those five sections as headings followed by their content. Nothing changes in the CMS or editorial process. Only the representation changes. The same applies to tabs, carousels, selectors and other interactive components. Only specialise where it is needed That doesn't mean every component needs a separate agent renderer. A heading is still a heading. A paragraph is still a paragraph. An image with useful alternative text might already be perfectly understandable in its normal HTML representation. Special handling becomes useful when human interaction makes information harder to access. An accordion is an obvious example. A carousel containing several products might be another. A comparison where information only appears after selecting different options is an even clearer case. So instead of asking how we create an AI version of every component, I think the more useful question is: Which components currently hides information behind human interaction? That gives us a much more manageable problem. What should the agent representation look like? I don't think there is a final answer yet. My starting point would still be good semantic HTML. It already works across the web and allows us to preserve hierarchy, links, media references and structured information without inventing a completely new publishing format. But the architecture shouldn't depend on semantic HTML alone. Markdown is interesting because it offers a simpler representation of textual content. Structured data can even provide extra context, and content negotiation could eventually allow a consumer to explicitly request another representation of the same resource. The important part is not whether the first version returns HTML or Markdown. It is having a clear point in the frontend where a representation can be created for a specific consumer. Keep it deterministic For the first version, I would keep the agent representation deliberately boring. No personalisation. No A/B testing. No user-specific state. No generative rewriting during the request. If the published CMS content hasn't changed, the output should ideally stay the same as well. That gives us a canonical representation which is easier to cache, test and monitor, while the approved CMS content remains the source of truth. This also prevents agent rendering from quietly becoming another content channel. The moment we start generating alternative marketing copy specifically for AI agents, we introduce new questions around approval, compliance, translations and ownership. That might become interesting later, but I wouldn't start there. Caching and testing become easier A stable representation also makes caching much more attractive. Human experiences often contain personalisation, experiments and other state that make responses vary between visitors. A public, canonical agent representation can avoid most of that. Instead of letting every agent execute the full frontend and trigger all backend calls used by the human experience, we may be able to serve a much smaller cached response. At the same time, we need to prove that simplifying the representation hasn't removed important information. Because both versions use the same source content, much of that testing can be automated. We can compare important text, links, media references, heading structure and structured data between both representations. The goal isn't to create the smallest possible page. It is to create the simplest representation that still preserves the meaning. The architecture I would propose today When designing a new headless DXP today, I wouldn't make agent rendering a completely separate subsystem. I would make it a capability of the rendering layer. The CMS would continue managing structured content, workflows, translations, permissions and publishing. The frontend would continue receiving component data, but it would no longer assume there is only one way to turn that data into an experience. Most components would have one representation (semantic HTML). Only the components where interaction hides information would need something different. Consumer detection could happen earlier at the CDN, edge or application level, while the rendering layer remains independent of how that decision was made. I am not suggesting this because we already know exactly what AI agents will need in the future. We don't. The value is that the architecture gives us a place to evolve without redesigning the frontend later. From architecture to implementation All of this still looks relatively clean on paper. The real test is what happens when we actually build it. How much additional code does an agent renderer require? Can we avoid maintaining two separate component trees? What happens to components that don't have a special agent representation? And how do we test that both versions still contain the same meaningful information? That is what I want to explore in part 3. Instead of another architecture diagram, I want to build a small agent-rendering implementation using a modern headless frontend. We can take a few typical interactive components, render them normally for a human visitor and then create a deterministic representation for an AI agent. That should show whether agent rendering remains a useful architectural idea once we actually have to maintain the code.

Read post