# Content rendering

Learn how to fetch and render content for your slots.

This guide provides practical examples of how to use the Symfony SDK to fetch and render a [Slot](/explanation/slot) in your application.

## Basic usage

Let's say you have a slot called `home-hero` for the hero section of your homepage.

First, add it to your project with the CLI:

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

```sh
croct add slot home-hero
```

The CLI downloads the slot's [default content](/explanation/content/slot-default-content) for use as automatic fallback and generates the [type definitions](/reference/cli/type-generation) for full autocomplete.

Inject the [`Plug`](/reference/sdk/php/api/core/plug) service into your controller and call [`fetchContent`](/reference/sdk/php/api/core/plug/fetch-content) with the slot ID. It returns a [response](/reference/sdk/php/api/response/fetch-response) with the resolved content:

**src/Controller/HomeController.php**

```php
<?php

namespace App\Controller;

use Croct\Plug\Plug;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class HomeController extends AbstractController
{
    #[Route('/', name: 'home')]
    public function index(Plug $croct): Response
    {
        $hero = $croct->fetchContent('home-hero')->getContent();

        return $this->render('home/index.html.twig', ['hero' => $hero]);
    }
}
```

The actual structure of the content depends on the [schema](/reference/content/schema/introduction) of the slot, but here is an example:

**Content response**

```json
{
"title": "The best personalization platform for developers",
"subtitle": "Best-in-class developer experience and enterprise-grade reliability.",
"image": {
  "url": "https://cdn.croct.io/devs.png",
  "alt": "A screenshot of an editor showing a code snippet."
},
"button": {
  "label": "See docs",
  "url": "https://docs.croct.com"
}
}
```

You can then render the content in your Twig template, which escapes output automatically:

**templates/home/index.html.twig**

```twig
<section class="hero">
    <h1>{{ hero.title }}</h1>
    <p class="subtitle">{{ hero.subtitle }}</p>
    <img src="{{ hero.image.url }}" alt="{{ hero.image.alt }}">
    <a href="{{ hero.button.url }}">{{ hero.button.label }}</a>
</section>
```

## Typing

The PHPStan and Psalm plugins shipped with the SDK infer the exact shape of every slot, so [`fetchContent`](/reference/sdk/php/api/core/plug/fetch-content) is typed with no manual annotations:

```php
$content = $croct->fetchContent('home-banner')->getContent();
$title = $content['title']; // string
```

The [CLI](/reference/cli/type-generation) writes these definitions to a `slots.stub` file whenever you add or update slots, and the plugins load it automatically.

## 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.

You should always provide a [fallback content](/explanation/content/fallback-content) to make your application resilient to unexpected errors, downtime, and network failures.

Pass a [fallback](/reference/sdk/php/api/options/fetch-options/with-fallback), and the SDK returns it whenever the content cannot be fetched:

```php
use Croct\Plug\FetchOptions;

$options = FetchOptions::defaults()->withFallback([
    'title' => 'Welcome to Croct!',
    'subtitle' => 'The easiest way to personalize your application.',
]);
$hero = $croct->fetchContent('home-hero', $options)->getContent();
```

Without a fallback, a failed request throws an [exception](/reference/sdk/php/api/exceptions/croct-exception), which you can catch to handle the error yourself.

## Version control

You can lock a specific slot version to keep the content structure aligned with your application's expectations. This gives your team the freedom to evolve the structure over time without the risk of breaking things.

Specify the version by passing a versioned ID in the form `<id>@<version>`. For example, passing `home-hero@2` fetches the content for the `home-hero` slot in version 2. Not specifying a version is the same as passing `home-hero@latest`, which loads the latest content:

```php
$content = $croct->fetchContent('home-hero@2')->getContent();
```

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

## Localization

The bundle [detects the visitor locale](/reference/sdk/symfony/data-collection#locale) from the request automatically, so content is returned in the matching locale out of the box.

To request a specific locale instead, use the [preferred locale](/reference/sdk/php/api/options/fetch-options/with-preferred-locale) option:

```php
use Croct\Plug\FetchOptions;

$options = FetchOptions::defaults()->withPreferredLocale('en-ca');
$hero = $croct->fetchContent('home-hero', $options)->getContent();
```

By default, 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

Sometimes you need to provide additional information to personalize or segment your users.

For example, if you are working on a SaaS application, you may want to personalize the content based on the subscription plan, quota usage, features, or any other application-specific information. You can achieve this by passing any relevant information as a custom [attribute](/reference/sdk/php/api/options/fetch-options/with-attribute):

```php
use Croct\Plug\FetchOptions;

$options = FetchOptions::defaults()->withAttribute('plan', 'premium');
$content = $croct->fetchContent('upgrade-banner', $options)->getContent();
```

These values are then accessible as custom attributes in the [context variable](/reference/cql/context#evaluation):

```cql
context's plan is "premium"
```

Keep in mind that the context has some constraints on the number of attributes and levels of nesting. For more information, see the [attribute](/reference/sdk/php/api/options/fetch-options/with-attribute) documentation.

## Caching \[#caching]

The bundle marks personalized responses as private automatically, so they are never stored in a shared cache. If you run a CDN or reverse proxy in front of your application, see [Caching](/reference/sdk/symfony/caching) for guidance.

## Explore

- [Slots](/explanation/slot): Learn how slots help you organize and personalize your content.
- [fetchContent method](/reference/sdk/php/api/core/plug/fetch-content): Explore the method documentation and available options.
