# Content rendering

Learn how to fetch and render content for your slots.

This guide shows how to fetch and render a [slot](/explanation/slot) in your Shopify Hydrogen storefront.

Because Hydrogen renders on the server, you [fetch content](/reference/sdk/hydrogen/api/functions/fetch-content) in your loaders and render it directly from the loader data. The page arrives already personalized, with no client-side flicker.

## Basic usage

Start by adding the slot using the CLI:

**Command to add a slot to your project**

```sh
croct@latest add slot --example
```

The CLI generates the TypeScript types and a working example you can adapt to your storefront.

> **How types and fallback content work**
>
> The CLI [generates type definitions](/reference/cli/type-generation) so that fetching a slot returns the correct content type with full autocomplete. It also [downloads default content](/reference/cli/fallback-content) that the SDK uses as automatic fallback when dynamic content is unavailable.

Fetch the content in your loader, then render it from the loader data:

**React Router 7 — JavaScript**

```jsx
import {useLoaderData} from 'react-router';
import {fetchContent} from '@croct/plug-hydrogen/server';

export async function loader({context}) {
  const {content} = await fetchContent('home-hero', {
    scope: context,
  });

  return {hero: content};
}

export default function Index() {
  const {hero} = useLoaderData();

  return (
    <div>
      <strong>{hero.title}</strong>
      <p>{hero.subtitle}</p>
      <a href={hero.button.link}>{hero.button.label}</a>
    </div>
  );
}
```

**React Router 7 — TypeScript**

```tsx
import {useLoaderData} from 'react-router';
import {fetchContent} from '@croct/plug-hydrogen/server';
import type {Route} from './+types/_index';

export async function loader({context}: Route.LoaderArgs) {
  const {content} = await fetchContent('home-hero', {
    scope: context,
  });

  return {hero: content};
}

export default function Index() {
  const {hero} = useLoaderData<typeof loader>();

  return (
    <div>
      <strong>{hero.title}</strong>
      <p>{hero.subtitle}</p>
      <a href={hero.button.link}>{hero.button.label}</a>
    </div>
  );
}
```

**Remix — JavaScript**

```jsx
import {useLoaderData} from '@remix-run/react';
import {fetchContent} from '@croct/plug-hydrogen/server';

export async function loader({context}) {
  const {content} = await fetchContent('home-hero', {
    scope: context,
  });

  return {hero: content};
}

export default function Index() {
  const {hero} = useLoaderData();

  return (
    <div>
      <strong>{hero.title}</strong>
      <p>{hero.subtitle}</p>
      <a href={hero.button.link}>{hero.button.label}</a>
    </div>
  );
}
```

**Remix — TypeScript**

```tsx
import {useLoaderData} from '@remix-run/react';
import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {fetchContent} from '@croct/plug-hydrogen/server';

export async function loader({context}: LoaderFunctionArgs) {
  const {content} = await fetchContent('home-hero', {
    scope: context,
  });

  return {hero: content};
}

export default function Index() {
  const {hero} = useLoaderData<typeof loader>();

  return (
    <div>
      <strong>{hero.title}</strong>
      <p>{hero.subtitle}</p>
      <a href={hero.button.link}>{hero.button.label}</a>
    </div>
  );
}
```

The following examples use the [React Router 7](https://reactrouter.com) loader signature. On [Remix](https://remix.run), change the loader arguments type as shown above. For content that must update on the client after the initial load, use the [content hook](/reference/sdk/hydrogen/api/hooks/use-content) or the [slot component](/reference/sdk/hydrogen/api/components/slot).

## Typing

If you are using TypeScript, use the [slot content type](/reference/sdk/hydrogen/api/types/slot-content) and [component content type](/reference/sdk/hydrogen/api/types/component-content) to type variables and props based on your slot or component schemas:

```tsx
import type {SlotContent} from '@croct/plug-hydrogen';

type HeroProps = SlotContent<'home-hero@1'>;
```

These types are automatically available when you add slots or components using the CLI. See [type generation](/reference/cli/type-generation) for details.

## Fault tolerance

> **Auto-provided**
>
> You can skip this step if you added the slot using the Croct CLI, since the fallback content is already included based on the slot's default content. See the [fallback hierarchy](/reference/cli/fallback-content#fallback-hierarchy) for how the SDK resolves content.

Always provide a [fallback content](/explanation/content/fallback-content) to make your storefront resilient to errors, downtime, and network failures. Specify the content to use when the dynamic content is unavailable:

**JavaScript**

```jsx
import {fetchContent} from '@croct/plug-hydrogen/server';

export async function loader({context}) {
  const {content} = await fetchContent('home-hero', {
    scope: context,
    fallback: {
      title: 'Welcome to Croct!',
      subtitle: 'The easiest way to personalize your storefront.',
      button: {
        label: 'Get started',
        link: '/',
      },
    },
  });

  return {hero: content};
}
```

**TypeScript**

```tsx
import {fetchContent} from '@croct/plug-hydrogen/server';
import type {Route} from './+types/_index';

export async function loader({context}: Route.LoaderArgs) {
  const {content} = await fetchContent('home-hero', {
    scope: context,
    fallback: {
      title: 'Welcome to Croct!',
      subtitle: 'The easiest way to personalize your storefront.',
      button: {
        label: 'Get started',
        link: '/',
      },
    },
  });

  return {hero: content};
}
```

## Version control

Lock a specific slot version to keep the content structure aligned with your storefront's expectations. Pass a versioned ID in the form `<id>@<version>`, such as `home-hero@2`. Omitting the version is the same as requesting the latest one:

**JavaScript**

```jsx
import {fetchContent} from '@croct/plug-hydrogen/server';

export async function loader({context}) {
  const {content} = await fetchContent('home-hero@2', {
    scope: context,
  });

  return {hero: content};
}
```

**TypeScript**

```tsx
import {fetchContent} from '@croct/plug-hydrogen/server';
import type {Route} from './+types/_index';

export async function loader({context}: Route.LoaderArgs) {
  const {content} = await fetchContent('home-hero@2', {
    scope: context,
  });

  return {hero: content};
}
```

For more information, see [slot versioning](/explanation/slot#versioning).

## Localization

By default, the SDK resolves the content locale from the storefront's internationalization configuration. To request a specific locale, pass the preferred locale in [BCP‑47](https://www.rfc-editor.org/info/bcp47) form:

**JavaScript**

```jsx
import {fetchContent} from '@croct/plug-hydrogen/server';

export async function loader({context}) {
  const {content} = await fetchContent('home-hero', {
    scope: context,
    preferredLocale: 'en-CA',
  });

  return {hero: content};
}
```

**TypeScript**

```tsx
import {fetchContent} from '@croct/plug-hydrogen/server';
import type {Route} from './+types/_index';

export async function loader({context}: Route.LoaderArgs) {
  const {content} = await fetchContent('home-hero', {
    scope: context,
    preferredLocale: 'en-CA',
  });

  return {hero: content};
}
```

If the content is not available in the preferred locale, it is returned in the [default locale of your workspace](https://app.croct.com/redirect/organizations/-organization-/workspaces/-workspace-/settings).

## Context variables \[#context-variables]

To personalize content with application-specific data, pass an evaluation context with custom attributes when you fetch. The values become available in the [context variable](/reference/cql/context#evaluation):

**JavaScript**

```jsx
import {fetchContent} from '@croct/plug-hydrogen/server';

export async function loader({context}) {
  const {content} = await fetchContent('upgrade-banner', {
    scope: context,
    context: {
      attributes: {
        plan: 'premium',
      },
    },
  });

  return {banner: content};
}
```

**TypeScript**

```tsx
import {fetchContent} from '@croct/plug-hydrogen/server';
import type {Route} from './+types/_index';

export async function loader({context}: Route.LoaderArgs) {
  const {content} = await fetchContent('upgrade-banner', {
    scope: context,
    context: {
      attributes: {
        plan: 'premium',
      },
    },
  });

  return {banner: content};
}
```

## Explore

- [Slots](/explanation/slot): Learn how slots help you organize and personalize your content.
- [fetchContent](api/functions/fetch-content): Explore the function documentation and available options.
