1. Content rendering

Render SitecoreAI content in your app

Help us improve this documentation

The framework-agnostic documentation is under development. If you have suggestions for improving the content, let us know by sharing your Feedback at the bottom of this page.

This walkthrough explains how to render SitecoreAI content in your front-end application, which is a key development task with Sitecore. By the end of the walkthrough, your front-end app will render SitecoreAI Page builder content in localhost.

The walkthrough provides code examples for Astro and Go front-end applications, and it describes how to:

  1. Configure your Sitecore environment variables
  2. Retrieve JSON-formatted layout data using GraphQL
  3. Set up page routing
  4. Build components
  5. Handle 404 pages
  6. Test your app
Before you begin

Configure your Sitecore environment variables

A series of identifiers are needed to request the layout for a single page. It's best practice to pass these identifiers into the GraphQL layout request as variables. In Node-based applications, these are typically stored in a .env file, but you should use the appropriate method for the language and framework you are building with.

Important

Secrets such as API keys, editing secrets, and Context IDs must never be hard-coded or exposed to the browser. It's best practice to store them in environment variables or a secrets manager instead. To interact with the GraphQL APIs, we recommend creating a scoped Context ID for Edge to limit sensitive API access to server-side operations.

NameDescriptionExample
SITECORE_EDGE_PLATFORM_URLThe Preview GraphQL API and Delivery GraphQL API endpoint.Set the value to the following:

https://edge-platform.sitecorecloud.io/v1/content/api/graphql/v1
SITECORE_EDGE_CONTEXT_IDYour environment's Preview Context ID or Live Context ID.

Find the value in SitecoreAI Deploy > Projects > your project > Authoring environments > your environment > Details.

Use the Preview Context ID to access both unpublished (draft) and published content, recommended for local development. Use the Live Context ID to access only published content.
0123456789abcdefghijkl
SITECORE_SITE_NAMEYour site's system name.

Find the value in SitecoreAI Deploy > Projects > your project > Authoring environments > your environment > Sites.
my-site
SITECORE_SITE_LANGUAGE_CODEYour site's language code. Find the value in SitecoreAI > Settings > Languages > Language code.

Use a code for a language that your site is available in. You can check available languages by opening your site in the SitecoreAI Page builder and clicking the language dropdown in the top toolbar.
  • en
  • de-DE
  • ja-JP

Retrieve JSON-formatted layout data using GraphQL

After configuring your environment variables, you can start making GraphQL queries. In this step, you retrieve the layout data (page representation) for your site's root page so you can inspect the raw JSON that SitecoreAI sends to your app.

The following GraphQL query lets you retrieve layout data for a page:

graphql
query LayoutQuery($site: String!, $language: String!, $routePath: String!) {
  layout(site: $site, language: $language, routePath: $routePath) {
    item {
      rendered # Returns layout data
    }
  }
}

This query requires that you specify the site the page belongs to, the language version of the page, and the path to the page.

Select your framework and follow the steps to make this query in your front-end app:

  1. To create the query, create src/services/sitecoreClient.js and paste the following code:

    astro
    // src/services/sitecoreClient.js
    
    export async function fetchLayoutData(site, language, routePath) {
      const env = import.meta.env;
    
      const endpoint = env.SITECORE_EDGE_PLATFORM_URL;
      const contextId = env.SITECORE_EDGE_CONTEXT_ID;
    
      // Create the GraphQL query:
      const query = `
        query LayoutQuery($site: String!, $language: String!, $routePath: String!) {
          layout(site: $site, language: $language, routePath: $routePath) {
            item { rendered }
          }
        }
      `;
    
      const variables = {
        site,
        language,
        routePath,
      };
    
      /* Optionally, add if-statements to check for missing environment variables. */
    
      // Include the Context ID in the header:
      const headers = {
        "Content-Type": "application/json",
        "x-sitecore-contextid": contextId,
      };
    
      // Make the GraphQL request:
      const response = await fetch(endpoint, {
        method: "POST",
        headers,
        body: JSON.stringify({ query, variables }),
      });
    
      const result = await response.json();
    
      if (result.errors) {
        throw new Error(JSON.stringify(result.errors));
      }
    
      const rendered = result?.data?.layout?.item?.rendered;
      return typeof rendered === "string" ? JSON.parse(rendered) : rendered;
    }
  2. To make the query when the root page of your front-end app loads, replace the contents of src/pages/index.astro with the following code:

    astro
    ---
    // src/pages/index.astro
    import { fetchLayoutData } from "../services/sitecoreClient.js";
    
    const site = import.meta.env.SITECORE_SITE_NAME;
    const language = import.meta.env.SITECORE_SITE_LANGUAGE_CODE || "en";
    const routePath = "/";
    const layoutData = await fetchLayoutData(site, language, routePath);
    ---
    
    <pre>{JSON.stringify(layoutData, null, 2)}</pre>

    When you open your project's root page in localhost, the page will make the GraphQL query and display the raw JSON that SitecoreAI returns.

  3. Start your development server and open your project's home page in localhost. The raw JSON displays. This is the actual page layout data for the Home page in the SitecoreAI Page builder.

json
{
  "sitecore": {
    "context": { /* metadata */ },
    "route": {
      "name": "Home",
      "displayName": "Home",
      "fields": {
        /* Page-level content fields (title, summary, thumbnail, keywords, navigation information, etc.) */
      },
      "placeholders": {
        "headless-header": [ /* Array of components for page header */ ],
        "headless-main": [ /* Array of components for page main section */ ],
        "headless-footer": [ /* Array of components for page footer */ ]
      }
    }
  }
}

The structure of a component:

json
{
  "uid": "<UNIQUE_COMPONENT_ID>",
  "componentName": "<COMPONENT_NAME>",
  "dataSource": "<DATA_SOURCE_PATH>",
  "params": { /* Display parameters (styles, modes) */ },
  "fields": { /* Content data for this component */ },
  "placeholders": {
    /* Nested children components (recursive structure) */
    "nested-placeholder-name": [ /* More components */ ]
  }
}

Set up page routing

Now that your app can retrieve layout data for a single path, you set up a catch-all route that retrieves layout data for any URL path and redirects to /404 when a path does not exist in Sitecore.

If you restart your development server, your front-end app in localhost will now render the raw JSON for the Home page (root), but it cannot render the JSON for other pages yet. You need to set up page routing to render all the URL paths of your site.

Note

The Home page (root) corresponds to a single forward-slash /, so you must always include a leading forward-slash in your routes. For example, to route to the Home > Products page, use the /products route, including the forward-slash in the beginning of the string.

The routing code in this step uses a simple redirect to /404 for routes that do not exist in Sitecore. This is an intentional placeholder. You will replace it with a proper Sitecore-managed 404 page in a later procedure.

Dynamic routes in Astro require server rendering or using getStaticPaths(). This walkthrough uses server rendering via export const prerender = false;.

  1. Create a catch-all route src/pages/[...slug].astro and implement the routing logic:

    astro
    ---
    // src/pages/[...slug].astro
    import { fetchLayoutData } from '../services/sitecoreClient.js';
    
    export const prerender = false;
    
    const site = import.meta.env.SITECORE_SITE_NAME;
    const language = import.meta.env.SITECORE_SITE_LANGUAGE_CODE || "en";
    const { slug } = Astro.params;
    const routePath = slug ? `/${slug}` : '/';
    
    let layoutData;
    try {
      layoutData = await fetchLayoutData(site, language, routePath);
    } catch (error) {
      return Astro.redirect('/404');
    }
    
    if (!layoutData) {
      return Astro.redirect('/404');
    }
    ---
    
    <pre>{JSON.stringify(layoutData, null, 2)}</pre>
  2. Restart your development server, and then check that a route different to the root, such as /about, now displays raw JSON for that page.

Build components

Build a component mapping and a missing component fallback

Before you implement individual components, you need two foundations in place: a component mapping that connects Sitecore componentName strings to your component implementations, and a fallback component that handles any componentName that is not yet in the mapping.

The component mapping ensures that when the layout data contains a component named "RichText", your app renders the right implementation. The fallback component ensures unmapped components are immediately visible during development because it renders an error box showing the component name.

Adding a new component is always the same two-step pattern: implement the component, then add it to the map. Every component in this walkthrough follows this pattern.

Handling missing content

When a componentName is not mapped in the component map, the Placeholder component renders the fallback instead of a real component. The fallback renders a visible, styled error box showing the component name. No nested placeholder children are rendered.

This makes missing components immediately visible during development so you know exactly which components still need to be implemented.

To make a missing component's content appear, implement the component so it renders its own fields, and then register it in the component map. After mapping, the real implementation replaces the error box.

To build a component mapping and a missing component fallback:

  1. Create src/components/componentMap.js and paste the following code:

    astro
    // src/components/componentMap.js
    // Import your component implementations here, then add them to the map.
    
    export const componentMap = {
      // Register component implementations here as you build them.
      // Example: 'ComponentName': ComponentImplementation
    };

    You will add entries to this map in later procedures as you implement components.

  2. Create src/components/MissingComponent.astro and paste the following code:

    astro
    ---
    // src/components/MissingComponent.astro
    
    const { componentName } = Astro.props;
    ---
    
    <div style="background: darkorange; outline: 5px solid orange; padding: 10px; color: white; max-width: 500px;">
      <h2>{componentName}</h2>
      <p>Component is not implemented. Add an implementation and register it in the component map.</p>
    </div>

Build the placeholder and page layout

With the component mapping and fallback in place, you can now build the Placeholder component and the page layout. The Placeholder component is the engine of the rendering pipeline: it takes a named placeholder slot and its array of components from the layout data, looks up each component in the mapping, and renders it. Because components can contain nested placeholders, Placeholder is called recursively throughout the page.

The page layout component connects the three top-level placeholders (headless-header, headless-main, headless-footer) to your Placeholder component and generates the final HTML page. All other placeholders on the page are rendered by the recursive Placeholder component.

After this step, your app routes all pages through the full rendering pipeline. Components won't display meaningful content yet because no component implementations are registered in the mapping. You'll implement components in the next procedures.

To build the placeholder and page layout:

  1. Create src/components/Placeholder.astro and paste the following code:

    astro
    ---
    // src/components/Placeholder.astro
    import { componentMap } from './componentMap.js';
    import MissingComponent from './MissingComponent.astro';
    
    const { name, rendering } = Astro.props;
    // Type casts are needed because the component map is keyed dynamically
    // and rendering items are untyped layout data.
    const map = componentMap as Record<string, any>;
    const items: any[] = rendering ?? [];
    ---
    
    <div data-placeholder={name}>
      {items.map((component) => {
        const Component = map[component.componentName] ?? MissingComponent;
    
        return (
          <Component
            componentName={component.componentName}
            fields={component.fields}
            params={component.params}
            placeholders={component.placeholders}
          />
        );
      })}
    </div>

    The Placeholder component takes a placeholder name and its component array, finds the right implementation for each componentName, and then passes through fields, params, and placeholders. This enables child components to render their own nested placeholders.

  2. Create src/components/Layout.astro and paste the following code:

    astro
    ---
    // src/components/Layout.astro
    import Placeholder from './Placeholder.astro';
    
    interface Props {
      layoutData: any;
    }
    
    const { layoutData } = Astro.props;
    const route = layoutData?.sitecore?.route;
    const placeholders = route?.placeholders || {};
    ---
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>{route.displayName || route.name}</title>
    </head>
    <body>
      {route ? (
        <>
          <header>
            <Placeholder
              name="headless-header"
              rendering={placeholders['headless-header']}
            />
          </header>
    
          <main>
            <Placeholder
              name="headless-main"
              rendering={placeholders['headless-main']}
            />
          </main>
    
          <footer>
            <Placeholder
              name="headless-footer"
              rendering={placeholders['headless-footer']}
            />
          </footer>
        </>
      ) : (
        <main>
          <p>Layout data is missing for this route.</p>
        </main>
      )}
    </body>
    </html>
  3. Update src/pages/index.astro to render the Layout component instead of raw JSON:

    astro
    ---
    // src/pages/index.astro
    import SitecoreLayout from '../components/Layout.astro';
    import { fetchLayoutData } from '../services/sitecoreClient.js';
    
    const site = import.meta.env.SITECORE_SITE_NAME;
    const language = import.meta.env.SITECORE_SITE_LANGUAGE_CODE || "en";
    const routePath = "/";
    const layoutData = await fetchLayoutData(site, language, routePath);
    ---
    
    <SitecoreLayout layoutData={layoutData} />
  4. Update src/pages/[...slug].astro to render the Layout component instead of raw JSON:

    astro
    ---
    // src/pages/[...slug].astro
    import { fetchLayoutData } from '../services/sitecoreClient.js';
    import SitecoreLayout from '../components/Layout.astro';
    
    export const prerender = false;
    
    const site = import.meta.env.SITECORE_SITE_NAME;
    const language = import.meta.env.SITECORE_SITE_LANGUAGE_CODE || "en";
    const { slug } = Astro.params;
    const routePath = slug ? `/${slug}` : '/';
    
    let layoutData;
    try {
      layoutData = await fetchLayoutData(site, language, routePath);
    } catch (error) {
      return Astro.redirect('/404');
    }
    
    if (!layoutData) {
      return Astro.redirect('/404');
    }
    ---
    
    <SitecoreLayout layoutData={layoutData} />

Implement built-in components

Components read their content from the fields object in the layout data. Each field has a type, and each type produces a different JSON shape. In this step, you implement three of the built-in Sitecore components: Title, RichText, and Image. Each component demonstrates reading and rendering one or more field types:

  • Title - renders a plain text field (heading). Example: { "value": "Hello World" }
  • RichText - renders a rich text field (Text). The value is HTML markup that must be injected as raw HTML, not escaped text. Example: { "value": "<p>HTML content</p>" }
  • Image - renders three fields that together make up the built-in Image component: an image field (Image), a plain text caption field (ImageCaption), and a General Link field (TargetUrl) that wraps the image in a link when set.

To implement built-in components:

  1. Create the Title component:

    astro
    ---
    // src/components/Title.astro
    interface Props {
      fields: {
        heading?: { value?: string };
      };
      params?: Record<string, any>;
    }
    
    const { fields, params } = Astro.props;
    ---
    
    {fields?.heading?.value && (
      <h1 class={params?.cssClass}>
        {fields.heading.value}
      </h1>
    )}
  2. Create the RichText component:

    astro
    ---
    // src/components/RichText.astro
    
    interface Props {
      fields: {
        Text?: { value?: string };
      };
    }
    
    const { fields } = Astro.props;
    const html = fields?.Text?.value ?? "";
    ---
    
    <div set:html={html} />
  3. Create the Image component:

    astro
    ---
    // src/components/Image.astro
    
    interface Props {
      fields: {
        Image?: {
          value?: { src?: string; alt?: string; width?: string; height?: string };
          jsonValue?: {
            value?: { src?: string; alt?: string; width?: string; height?: string };
          };
        };
        ImageCaption?: { value?: string };
        TargetUrl?: {
          value?: { href?: string; target?: string };
          jsonValue?: { value?: { href?: string; target?: string } };
        };
      };
    }
    
    const { fields } = Astro.props;
    const imageField = fields?.Image;
    const imageValue = imageField?.value ?? imageField?.jsonValue?.value;
    const { src, alt, width, height } = imageValue ?? {};
    const caption = fields?.ImageCaption?.value;
    const linkField = fields?.TargetUrl;
    const link = linkField?.value ?? linkField?.jsonValue?.value;
    ---
    
    {src && (
      <figure>
        {link?.href ? (
          <a href={link.href} target={link.target || undefined}>
            <img src={src} alt={alt ?? ''} width={width} height={height} />
          </a>
        ) : (
          <img src={src} alt={alt ?? ''} width={width} height={height} />
        )}
        {caption && <figcaption>{caption}</figcaption>}
      </figure>
    )}
  4. Register all three components in the component mapping by updating src/components/componentMap.js:

    javascript
    // src/components/componentMap.js
    import Image from './Image.astro';
    import RichText from './RichText.astro';
    import Title from './Title.astro';
    // Import all your other components here
    
    export const componentMap = {
      'Image': Image,
      'RichText': RichText,
      'Title': Title,
      // Map all your other components here
    };

Implement the Promo component

Promo is a content component that renders rich text from one of several fields (PromoText, PromoText2, or PromoText3). It demonstrates how a component can fall back across multiple field names when looking for content to display.

To implement the Promo component:

  1. Create the Promo component:

    astro
    ---
    // src/components/Promo.astro
    
    interface Props {
      fields: {
        PromoText?: { value?: string };
        PromoText2?: { value?: string };
        PromoText3?: { value?: string };
      };
    }
    
    const { fields } = Astro.props;
    const html =
      fields?.PromoText?.value ||
      fields?.PromoText2?.value ||
      fields?.PromoText3?.value ||
      "";
    ---
    
    {html && <div set:html={html} />}
  2. Register Promo in the component mapping by updating src/components/componentMap.js with the following import and entry:

    javascript
    import Promo from './Promo.astro';
    
    export const componentMap = {
      // ...existing entries...
      'Promo': Promo,
    };

Implement the ColumnSplitter component

ColumnSplitter is a structural component that manages nested placeholders rather than rendering its own field content. It splits its content area into columns and exposes each column as a named placeholder, allowing content authors to place other components inside each column. Implementing ColumnSplitter reinforces the recursive rendering concept established earlier in this walkthrough. The Placeholder component handles each column's contents, so any component registered in the component map can be nested inside a column.

To implement the ColumnSplitter component:

  1. Create the ColumnSplitter component:

    astro
    ---
    // src/components/ColumnSplitter.astro
    import Placeholder from './Placeholder.astro';
    
    interface Props {
      params?: Record<string, any>;
      placeholders?: Record<string, any[]>;
    }
    
    const { params, placeholders } = Astro.props;
    const enabled = typeof params?.EnabledPlaceholders === 'string'
      ? params.EnabledPlaceholders.split(',').map((value) => value.trim()).filter(Boolean)
      : null;
    type Column = { key: string; items: any[]; index: number };
    const columns = (Object.entries(placeholders ?? {})
      .map(([key, items]) => {
        const match = key.match(/^column-(\d+)-/);
        if (!match) return null;
    
        return {
          key,
          items,
          index: Number(match[1]),
        };
      })
      .filter((c): c is Column => c !== null)
      .filter((column) => !enabled || enabled.includes(String(column.index)))
      .sort((a, b) => a.index - b.index));
    const wrapperClass = [params?.GridParameters].filter(Boolean).join(' ');
    ---
    
    <div data-component="ColumnSplitter" class={wrapperClass || undefined}>
      {columns.map((column) => {
        const columnClass = params?.[`ColumnWidth${column.index}`];
    
        return (
          <div data-column={column.index} class={columnClass || undefined}>
            <Placeholder name={column.key} rendering={column.items} />
          </div>
        );
      })}
    </div>
  2. Register ColumnSplitter in the component mapping by updating src/components/componentMap.js with the following import and entry:

    javascript
    import ColumnSplitter from './ColumnSplitter.astro';
    
    export const componentMap = {
      // ...existing entries...
      'ColumnSplitter': ColumnSplitter,
    };

Implement the Container and PartialDesignDynamicPlaceholder components

Container and PartialDesignDynamicPlaceholder are structural components you will encounter on most Sitecore pages. Like ColumnSplitter, they render nested placeholder slots rather than field content.

  • Container wraps a set of nested placeholders in a <div> element and accepts optional CSS class parameters (Styles or CssClass). Content authors use it to group and style sections of a page.
  • PartialDesignDynamicPlaceholder is a transparent pass-through used by partial designs, which are reusable page sections that content authors configure in the Page builder and share across many pages. It renders all of its nested placeholder slots with no wrapper element.

To implement the Container and PartialDesignDynamicPlaceholder components:

  1. Create the Container component:

    astro
    ---
    // src/components/Container.astro
    import Placeholder from './Placeholder.astro';
    
    interface Props {
      params?: Record<string, any>;
      placeholders?: Record<string, any[]>;
    }
    
    const { params, placeholders } = Astro.props;
    const cssClass = params?.Styles ?? params?.CssClass ?? '';
    ---
    
    <div class={cssClass || undefined}>
      {Object.entries(placeholders ?? {}).map(([name, items]) => (
        <Placeholder name={name} rendering={items} />
      ))}
    </div>
  2. Create the PartialDesignDynamicPlaceholder component:

    astro
    ---
    // src/components/PartialDesignDynamicPlaceholder.astro
    import Placeholder from './Placeholder.astro';
    
    interface Props {
      placeholders?: Record<string, any[]>;
      params?: Record<string, any>;
    }
    
    const { placeholders } = Astro.props;
    ---
    
    {Object.entries(placeholders ?? {}).map(([name, items]) => (
      <Placeholder name={name} rendering={items} />
    ))}
  3. Register both components in the component mapping by updating src/components/componentMap.js with the following imports and entries:

    javascript
    import Container from './Container.astro';
    import PartialDesignDynamicPlaceholder from './PartialDesignDynamicPlaceholder.astro';
    
    export const componentMap = {
      // ...existing entries...
      'Container': Container,
      'PartialDesignDynamicPlaceholder': PartialDesignDynamicPlaceholder,
    };

Handle 404 pages

When a site visitor navigates to a path that doesn't exist in Sitecore, the layout query returns null for layout.item. The routing code you set up in a previous procedure handles this as a fallback to /404. For a more complete implementation, you can render the Sitecore-managed 404 page instead of redirecting to a generic path.

To handle 404 pages:

  1. Retrieve the notFoundPagePath and serverErrorPagePath that content authors have configured in the Page builder. You can do this by using the site query's errorHandling field.
  2. Using the fetchLayoutData function, retrieve and render the layout data for the paths you retrieved in the previous step.
  3. Return the response with an HTTP 404 status code so that browsers and search engines treat it correctly as a not-found response.

Test your app

You can now test your front-end app in localhost.

To test your app:

  1. Save all changes in your code editor and restart your development server.
  2. In localhost, navigate to the root route at / to render your Home page.
  3. Verify that Sitecore content is being rendered.
  4. Test your nested routes, such as /products and products/product-item-1.

Next steps

You've now rendered Sitecore content in your front-end app. Your app:

  • Queries Sitecore for page data.
  • Parses the recursive layout structure.
  • Renders components based on layout data.
  • Handles nested components and placeholders.

Next, you can:

  • Continue building and mapping component implementations, such as Accordion, Header, Footer, and your custom components so that every componentName in layout data correctly renders its fields. As you build each component, make sure to handle its field types.
  • Style your front-end app using CSS or a CSS framework of your choice.
  • For components that need data not included in the page layout, such as navigation menus, footer links, or listing data, make other GraphQL queries using the item, search, or site entry points. Retrieve this data separately from the layout query and pass the results into the relevant component. Keeping global data out of the per-page layout also improves publishing performance.
  • Learn more about the GraphQL APIs, including their differences, limitations, and best practices.
If you have suggestions for improving this article, let us know!