Skip to navigation

Use destinations in your app

Early access notice

The Destinations Delivery API is currently in early access while development is ongoing. Before integrating, please keep the following in mind:

  • Performance and SLOs: Response times do not yet meet standard Contentful SLOs, and some queries have latencies too high for production delivery traffic. We are aware of this limitation and are actively working on performance optimizations. (Note: Fetching experiences by ID rather than destination does meet our response-time SLOs.)
  • Potential breaking changes: Resource schemas, payload structures, and endpoints are subject to change prior to General Availability (GA).

Destinations can be consumed in the following ways:

  • During static site generation.
  • At render time using the client’s path.
  • With a hard-coded path or destination node ID.

Each example in this guide is shown with the Experiences SDK, which wraps the API calls and resolves the experience for you, and with the underlying API requests. The SDK examples use @contentful/experiences-react.

Static site generation

To statically generate an entire site for a destination, first retrieve its sitemap.

fetchDestinationSitemap lists the paths in a destination.

import { fetchDestinationSitemap } from '@contentful/experiences-react';
const spaceId = process.env.SPACE_ID!;
const destinationId = process.env.DESTINATION_ID!;
const accessToken = process.env.CDA_TOKEN!;
export async function listDestinationPaths() {
const sitemap = await fetchDestinationSitemap(
{ spaceId, destinationId },
{ accessToken },
);
return sitemap.items.map((item) => item.path);
}

Each item has the shape { path, experienceId, type }. The SDK extracts experienceId from the API’s experienceUrn.

From here, there are two ways to retrieve each experience:

  • Resolve each path through the Destinations Delivery API.
  • Use the experience ID from the sitemap (the experienceUrn in the raw response) and request it from the Experience Delivery API.

These approaches differ in how they use the destination context.

The Destinations Delivery API resolves an experience within its destination context. When an experience is published, its delivery artifacts are updated asynchronously. A destination node tracks which delivery artifacts belong to its linked experience, so resolving through the destination keeps the node and experience artifacts consistent.

Prefer this approach unless measured performance requirements justify bypassing the destination context.

With the SDK, pass the destinationId and a path from the sitemap to fetchExperience. The raw equivalent is the POST .../experiences request shown in Using the client’s path.

import { fetchExperience } from '@contentful/experiences-react';
import { experienceConfig } from './experience-config';
const result = await fetchExperience(
{ spaceId, destinationId, path: '/products/shoes' },
{ accessToken },
{ config: experienceConfig },
);
if ('redirect' in result) {
// The path is a redirect node. Fetch result.redirect.path instead.
} else {
// result is the resolved experience.
}

Using the Experience Delivery API

Resolving an experience directly through the Experience Delivery API avoids the destination tree lookup and may reduce latency, but it bypasses the destination context and its consistency guarantees. Consider this tradeoff on a case-by-case basis, and prefer the Destinations Delivery API when possible.

With the SDK, pass the sitemap item’s experienceId to fetchExperience. Unlike destination fetches, this call takes an environmentId:

const experience = await fetchExperience(
{ spaceId, environmentId: 'master', experienceId: item.experienceId },
{ accessToken },
{ config: experienceConfig },
);

Using the client’s path

Experiences can be resolved at request time, either server-side or client-side, using the client’s path.

For example, a site has the following pages:

/
├── products
│ ├── shoes
│ │ ├── running
│ │ └── hiking
│ └── accessories
├── about
└── contact

The Destinations Delivery API can resolve the experience for the current path:

curl --request POST \
--url "https://xdn.contentful.com/spaces/{space_id}/destinations/{destination_id}/experiences" \
--header "Authorization: Bearer {access_token}" \
--header "Content-Type: application/json" \
--data '{
"path": "/products/shoes/running"
}'

In a client-side React application, this could look like:

import { useEffect, useState } from "react";
import {
ClientExperienceRenderer,
NotFoundError,
fetchExperience,
type PortableRenderPlan,
} from "@contentful/experiences-react";
import { experienceConfig } from "./experience-config";
const spaceId = "<space_id>";
const destinationId = "<destination_id>";
const accessToken = "<access_token>";
export default function App() {
const [experience, setExperience] = useState<PortableRenderPlan | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
// Ignore a fetch that resolves after the component unmounts.
let isCurrent = true;
async function loadExperience() {
const result = await fetchExperience(
{ spaceId, destinationId, path: window.location.pathname }, // [1]
{ accessToken },
{ config: experienceConfig },
);
if (!isCurrent) return;
if ("redirect" in result) {
window.location.replace(result.redirect.path); // [2]
return;
}
setExperience(result);
}
loadExperience().catch((err) => {
if (!isCurrent) return;
setError(err instanceof NotFoundError ? new Error("Page not found") : err); // [3]
});
return () => {
isCurrent = false;
};
}, []);
if (error) {
return <p>{error.message}</p>;
}
if (!experience) {
return <p>Loading...</p>;
}
return (
<ClientExperienceRenderer
experience={experience}
config={experienceConfig}
/>
);
}

The notable parts of this example are:

  1. fetchExperience reads the path from window.location.pathname. The pathname should exactly match a resolved destination node path.
  2. A redirect node resolves to a DestinationRedirectResult, { redirect: { path } }, instead of an experience. This example navigates the browser to that path; alternatively, the application can call fetchExperience again with it.
  3. NotFoundError means no destination node claims the path. Render your not-found state.

Because the fetch runs in the browser, the access token ships in your bundle. Use a token that’s safe to expose, or fetch on the server. See Fetch experiences.

import { useEffect, useState } from "react";
import {
ClientExperienceRenderer,
resolveExperience,
} from "@contentful/experiences-react";
import { experienceConfig } from "./experience-config";
const spaceId = "<space_id>";
const destinationId = "<destination_id>";
const accessToken = "<access_token>";
export default function App() {
const [experience, setExperience] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
async function loadExperience() {
const response = await fetch(
`https://xdn.contentful.com/spaces/${spaceId}/destinations/${destinationId}/experiences`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ path: window.location.pathname }), // [1]
},
);
if (!response.ok) {
throw new Error(`Unable to resolve experience: ${response.status}`);
}
const result = await response.json();
if ("redirect" in result) {
window.location.assign(result.redirect.path); // [2]
return;
}
const destinationExperience = result.experiences[0];
if (!destinationExperience) {
throw new Error("The destination did not resolve an experience");
}
const resolvedExperience = await resolveExperience(
destinationExperience.experience,
experienceConfig,
);
setExperience(resolvedExperience);
}
loadExperience().catch(setError);
}, []);
if (error) {
return <p>{error.message}</p>;
}
if (!experience) {
return <p>Loading...</p>;
}
return (
<ClientExperienceRenderer
experience={experience}
config={experienceConfig}
/>
);
}

The notable parts of this example are:

  • The request reads the path from window.location.pathname and sends it to the Destinations Delivery API. The pathname should exactly match a resolved destination node path.
  • If the response is a redirect, it contains the target path. This example navigates the browser to that path; alternatively, the application can make another Destinations Delivery API request for it.

Hard-coding a path or destination node ID

The final option is to hard-code a path or destination node ID. A hard-coded path uses the same path-resolution endpoint shown above. A stable destination node ID can be preferable to an experience ID for two reasons:

  • The upsert operation lets you choose a meaningful, stable destination node ID.
  • The destination node can remain the same when its linked experience changes, so the client doesn’t need to be updated when editors replace the experience.

To create a destination experience node with a chosen ID, use the Destinations Management API upsert operation. This operation requires a Contentful management token, which must not be exposed in a client-side application:

curl --request PUT \
--url "https://api.contentful.com/spaces/{space_id}/environments/{environment_id}/destinations/{destination_id}/nodes/product-detail-page" \
--header "Authorization: Bearer {management_token}" \
--header "Content-Type: application/vnd.contentful.management.v1+json" \
--data '{
"type": "DestinationExperienceNode",
"properties": [],
"experience": {
"sys": {
"type": "ResourceLink",
"linkType": "Contentful:Experience",
"urn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/{experience_id}"
}
}
}'

Publish the destination node before requesting it from the Destinations Delivery API. Use the version returned by the upsert response, and make sure the destination is also published:

curl --request PUT \
--url "https://api.contentful.com/spaces/{space_id}/environments/{environment_id}/destinations/{destination_id}/nodes/product-detail-page/published" \
--header "Authorization: Bearer {management_token}" \
--header "X-Contentful-Version: {node_version}"

The client can then resolve the experience using the hard-coded destination node ID:

Pass nodeId instead of path. Redirects and NotFoundError are handled the same way as in the previous example.

import { useEffect, useState } from "react";
import {
ClientExperienceRenderer,
fetchExperience,
type PortableRenderPlan,
} from "@contentful/experiences-react";
import { experienceConfig } from "./experience-config";
const spaceId = "<space_id>";
const destinationId = "<destination_id>";
const accessToken = "<access_token>";
const nodeId = "product-detail-page";
export default function App() {
const [experience, setExperience] = useState<PortableRenderPlan | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let isCurrent = true;
async function loadExperience() {
const result = await fetchExperience(
{ spaceId, destinationId, nodeId },
{ accessToken },
{ config: experienceConfig },
);
if (!isCurrent) return;
if ("redirect" in result) {
window.location.replace(result.redirect.path);
return;
}
setExperience(result);
}
loadExperience().catch((err) => {
if (isCurrent) setError(err);
});
return () => {
isCurrent = false;
};
}, []);
if (error) {
return <p>{error.message}</p>;
}
if (!experience) {
return <p>Loading...</p>;
}
return (
<ClientExperienceRenderer
experience={experience}
config={experienceConfig}
/>
);
}

Examples

The examples directory in the Experiences SDK repository has runnable apps that serve a destination at the root path. Each example’s README explains how to seed a destination with its bootstrap script.