Using in your app

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.

Static site generation

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

curl --request GET \
--url "https://xdn.contentful.com/spaces/{space_id}/destinations/{destination_id}/sitemap" \
--header "Authorization: Bearer {access_token}"

The response contains one page of resolved paths and experience URNs:

{
"property": {
"sys": {
"type": "Destination"
}
},
"paths": {
"sys": {
"type": "Array"
},
"limit": 100,
"items": [
{
"sys": {
"type": "DestinationExperienceNode"
},
"path": "/",
"experienceUrn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/homepage"
},
{
"sys": {
"type": "DestinationExperienceNode"
},
"path": "/products",
"experienceUrn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/productIndex"
},
{
"sys": {
"type": "DestinationExperienceNode"
},
"path": "/products/shoes",
"experienceUrn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/shoeIndex"
},
{
"sys": {
"type": "DestinationExperienceNode"
},
"path": "/products/shoes/running",
"experienceUrn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/runningShoes"
},
{
"sys": {
"type": "DestinationExperienceNode"
},
"path": "/products/shoes/hiking",
"experienceUrn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/hikingShoes"
},
{
"sys": {
"type": "DestinationExperienceNode"
},
"path": "/products/accessories",
"experienceUrn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/accessories"
},
{
"sys": {
"type": "DestinationExperienceNode"
},
"path": "/about",
"experienceUrn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/aboutPage"
},
{
"sys": {
"type": "DestinationExperienceNode"
},
"path": "/contact",
"experienceUrn": "crn:contentful:::experience:spaces/$self/environments/$self/experiences/contactPage"
}
],
"pages": {}
}
}

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

  • Resolve each path through the Destinations Delivery API.
  • Extract the experience ID from experienceUrn 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.

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.

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,
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 '{
"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:

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>";
const nodeId = "product-detail-page";
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}/nodes/${nodeId}/experiences`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
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);
return;
}
const destinationExperience = result.experiences[0];
if (!destinationExperience) {
throw new Error("The destination node 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}
/>
);
}