cd /news/developer-tools/placeholder-maintenance-with-sitecor… · home topics developer-tools article
[ARTICLE · art-33995] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Placeholder Maintenance with Sitecore Content SDK

A developer working with Sitecore's Content SDK during a migration from JSS to XM Cloud encountered a challenge where server components cannot be placed inside a client component's placeholder. The solution involves creating a server component wrapper that owns the AppPlaceholder, allowing the Sitecore editor to insert any component type, while the client component handles interactivity.

read5 min views2 publishedJun 19, 2026

During the conversion process from JSS to Content SDK for Sitecore AI (IE XM Cloud), there's some changes to how components are designated and mapped to be aware of, which affect how placeholders work and how those components work inside the placeholders. This led to our needing a solution to handle placeholders in certain circumstances.

The component designation in Content SDK separates server from client components. This separation is determined by the use of hooks in the code (like useEffect

) and a component is made "client" by placing "use client"

in the first line of the component. Technically you could make every component "client" but you lose out on benefits of server components.

When it comes to Sitecore, there's a useSitecore

hook that can be used for getting things like the editing state. But you don't need to use that hook, because the new placeholder model, called AppPlaceholder

, that passes the page model down from one level to the next. That includes the edit state, so you can access it in server components as well. This starts all the way up in your Layout.tsx

file, which you'll see in the starter kit you can download from Github, and it's a required parameter so it'll always be passed down.

You cannot bring server components into a placeholder under a client component. Sitecore has documentation on this. It'll work if you set things up manually, but in Pages you won't be allowed to pull components in. To be clear, the scenario is you have a component - let's say a tabs component - that has an AppPlaceholder

on it. The tabs component will typically have some hooks to handle click changes between the tabs, and that requires "use client"

to be designated. Then you'll have a tabs panel component, which you pull in to set up each individual tab, and that also has AppPlaceholder

to hold a variety of other components. Then you might pull in a rich text component, which is likely a server component because it has no hooks. If you just do that straight away, like you would have in JSS, you won't be able to bring those server components in.

I'll say a solution because I'm sure there's others, and the documentation suggests something, but it wasn't entirely clear to me. What I worked out with some research, AI, and trial and error, was this solution. The solution boils down to this: if you have a component that has AppPlaceholder

and needs to be a client component as well, you have a client/server tag team.

The server component part is the actual component you'll be registering in Sitecore as a rendering. It might look something like this:

import { type JSX } from "react";
import { AppPlaceholder } from "@sitecore-content-sdk/nextjs";
import componentMap from ".sitecore/component-map";
import TabsClient from "./TabsClient";
import type { TabsProps } from "./Tabs.types";

/**
 * Tabs — server component wrapper.
 *
 * Owns AppPlaceholder so the Sitecore editor treats the placeholder as
 * server-owned and allows inserting any component type (server or client).
 *
 * Edit mode: renders the placeholder in a single column with a label.
 *
 * Normal mode: passes pre-rendered placeholder content as RSC children to
 * TabsClient, which handles mobile detection and DOM manipulation for the
 * horizontal tab navigation.
 */
export default function Tabs({ rendering, params, page }: TabsProps): JSX.Element {
  const isPageEditing = page?.mode?.isEditing ?? false;
  const phKey = `tabs-${params?.DynamicPlaceholderId ?? ""}`;

  const content = (
    <AppPlaceholder
      name={phKey}
      rendering={rendering}
      componentMap={componentMap}
      page={page}
    />
  );

  if (isPageEditing) {
    return (
      <>
        <section
          id={params?.RenderingIdentifier ?? ""}
          className={`component tabs ${params?.Styles ?? ""}`}
        >
          <div className="container">
            <div className="col-12">
              <h3>Tabs (edit mode)</h3>
              {content}
            </div>
          </div>
        </section>
      </>
    );
  }

  return (
    <>
      <TabsClient
        renderingUid={rendering.uid ?? ""}
        params={params}
      >
        {content}
      </TabsClient>
    </>
  );
}

You'll notice that this takes an AppPlaceholder

component and sends it to a component called TabsClient

. That's what we'll look at next, and where all the hooks are working at. But this provides a mechanism by which we continue the "chain" of server components, which allows us to use server or client components in the subsequent placeholder tree.

In our case, the tabs component is designed to switch to an accordion if in mobile mode, so there's a useEffect

and other hooks to handle resize checks. It also takes that children

parameter, which is our AppPlaceholder

fed down from the server component, and now it's safe to use server or client components. You can see all this in the code below.

"use client";

import {
  useCallback,
  useEffect,
  useRef,
  useState,
  type JSX,
  type ReactNode,
} from "react";
import type { TabsParams } from "./Tabs.types";

interface TabsClientProps {
  children: ReactNode;
  renderingUid: string;
  params?: TabsParams;
}

/**
 * TabsClient — handles mobile detection and horizontal tab nav DOM
 * manipulation for Tabs. Receives pre-rendered placeholder content as
 * children from the server component wrapper.
 *
 * After mount, moves .navItem <li> elements from the content area into the
 * nav <ul> and activates the first tab.
 */
export default function TabsClient({
  children,
  renderingUid,
  params,
}: TabsClientProps): JSX.Element {
  const containerRef = useRef<HTMLUListElement>(null);
  const [isMobile, setIsMobile] = useState(false);

  const handleResize = useCallback(() => {
    setIsMobile(window.innerWidth < 576);
  }, []);

  useEffect(() => {
    handleResize();
    window.addEventListener("resize", handleResize);

    if (!isMobile) {
      const runMove = () => {
        const elements = document.querySelectorAll(
          `.content-${renderingUid} .navItem`,
        );
        elements.forEach((el) => {
          if (el === elements[0]) {
            const anchor = el.getElementsByTagName("a")[0];
            anchor?.classList.add("active");
            anchor?.setAttribute("aria-selected", "true");

            const panel = document.querySelector(
              `.content-${renderingUid} .tab-pane`,
            );
            panel?.classList.add("active");
            panel?.classList.add("show");
          }
          containerRef.current?.appendChild(el);
        });
      };
      requestAnimationFrame(runMove);
    }

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, [handleResize, isMobile, renderingUid]);

  if (isMobile) {
    return (
      <section className="component accordions" data-module="accordions">
        <div className="container">
          <div className="box-shadow">{children}</div>
        </div>
      </section>
    );
  }

  return (
    <section
      id={params?.RenderingIdentifier ?? ""}
      className={`component tabs ${params?.Styles ?? ""}`}
    >
      <div className="container">
        <div className="row">
          <div className="col-12">
            <ul
              className={`nav nav-tabs tabs-${renderingUid}`}
              ref={containerRef}
              role="tablist"
            />
            <div className={`tab-content content-${renderingUid}`}>
              {children}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

This will hopefully provide developers with a starting point for handling placeholders where a client component is required. It should be pretty straightforward to engineer in your handling as appropriate, or to feed the example to AI to help you generate what you need.

Remember, if your AppPlaceholder

component doesn't require hooks, you don't need to do any of this extra work. Keep it as simple as possible!

── more in #developer-tools 4 stories · sorted by recency
── more on @sitecore 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/placeholder-maintena…] indexed:0 read:5min 2026-06-19 ·