Rendering Recommendations: From Content IDs to Story Cards
Audience: Front-end developers integrating Arc XP recommendations into a website or app.
Stack used in examples: TypeScript + React.
What this guide covers
The Recommender API returns a ranked list of recommendations, each carrying a card object with display fields sourced from the underlying content document. There is no separate inflation call: one round trip gives you everything needed to render story cards.
- Recommend — call
GET /recommend/v1/recommendationsto get a ranked list of items, each with acard. - Render — map each
cardonto a card component.
This guide walks through each step, gives you a runnable TypeScript + React snippet, and shows how to handle empty results and error states gracefully.
Prerequisites
- A site you can issue requests for (you’ll need its
site_id). - A way to identify the current viewer (you’ll pass that as
user_id— anonymous IDs are fine, as long as they’re stable per visitor). - An HTTP client. The examples use
fetch; any client is fine.
Step 1: Fetch recommendations
Endpoint
GET https://<your-org>-config-prod.api.arc-cdn.net/recommend/v1/recommendationsQuery parameters
| Name | Required | Description |
|---|---|---|
site_id | yes | The site you’re requesting recommendations for. |
user_id | yes | Stable identifier for the current viewer (logged-in or anonymous). |
num_results | no | How many recommendations to return. 1–50, default 5. |
item_id | no | Anchor item for “more like this” use cases. |
subscription_tier | no | Caller subscription level (e.g. "free", "premium"). |
device_type | no | Device class (e.g. "mobile", "desktop"). |
Response shape
{ "recommendations": [ { "item_id": "ABC123…", "score": 0.87, "card": { "title": "Election results upend mayoral race", "date": "2026-06-01T12:00:00Z", "author": "Alice Reporter", "type": "article", "is_premium": false, "categories": ["politics"], "tags": ["election", "2026"], "url": "https://example.com/story", "thumbnail_url": "https://cdn.example.com/story.jpg" } }, { "item_id": "DEF456…", "score": 0.81, "card": null } ], "attribution": { "exposure_id": "exp-…", "issued_at": "…" }}score is the post-reranking relevance score (0.0–1.0). Editorial signals — boosts, buries, and pins — have already been applied. You can use score to drive UI affordances (badging the top result, sorting, etc.), but the array order is already the recommended display order.
card is the display payload. Every field on the card is optional, and card itself is null when no matching content document was found (for example, an editor-pinned item whose document has not yet been synced). Cards should degrade gracefully when individual fields are missing — the only safe assumption is that a recommendation always has item_id and score.
TypeScript example
type ItemCard = { title: string | null; date: string | null; author: string | null; type: "article" | "video" | "podcast" | null; is_premium: boolean | null; categories: string[]; tags: string[]; url: string | null; thumbnail_url: string | null;};
type ScoredItem = { item_id: string; score: number; card: ItemCard | null;};
type RecommendationResponse = { recommendations: ScoredItem[];};
async function fetchRecommendations(params: { org: string; siteId: string; userId: string; numResults?: number;}): Promise<ScoredItem[]> { const url = new URL(`https://${params.org}-config-prod.api.arc-cdn.net/recommend/v1/recommendations`); url.searchParams.set("site_id", params.siteId); url.searchParams.set("user_id", params.userId); if (params.numResults) { url.searchParams.set("num_results", String(params.numResults)); }
const response = await fetch(url, { headers: { Accept: "application/json" }, });
if (!response.ok) { throw new Error(`Recommendations request failed: ${response.status}`); }
const body = (await response.json()) as RecommendationResponse; return body.recommendations;}Step 2: Map to card fields
A usable story card needs four fields at minimum:
| Card field | Source field | Notes |
|---|---|---|
| Headline | card.title | Display title for the card. |
| Image | card.thumbnail_url | Hero/lead image. Use a CDN-resized variant if your design system has one. |
| URL | card.url | The link the card navigates to on click. |
| Byline | card.author | Author display string. Fall back to publication name if absent. |
Anything beyond those four (kicker, card.date, section label, premium badge from card.is_premium) is layered on top of this minimum set. Build cards that degrade gracefully when an optional field is missing, and skip rendering when card is null or the required fields are missing.
type StoryCardProps = { item: ScoredItem;};
function StoryCard({ item }: StoryCardProps) { const card = item.card; if (!card || !card.url || !card.title) { return null; } return ( <a href={card.url} className="story-card"> {card.thumbnail_url ? <img src={card.thumbnail_url} alt="" /> : null} <h3>{card.title}</h3> {card.author ? <p className="byline">{card.author}</p> : null} </a> );}Handling empty results and errors
There are three states to design for. Don’t render a half-broken card grid in any of them.
| State | When it happens | Recommended UI |
|---|---|---|
| Empty | The API returns recommendations: []. Common for new sites, new users, cold-start scenarios. | Show a fallback rail (editorial picks, most-read) — never an empty grid. |
| Network error | The recommendations request fails (timeout, 5xx, network). | Show the same fallback rail. Log the error for observability. |
| Validation | The API returns 422. Almost always a caller bug (missing/invalid params). | Surface during development. In production, treat as the network-error case. |
function RecommendationsRail({ org, siteId, userId }: { org: string; siteId: string; userId: string }) { const [items, setItems] = useState<ScoredItem[] | null>(null); const [error, setError] = useState<Error | null>(null);
useEffect(() => { let cancelled = false; (async () => { try { const scored = await fetchRecommendations({ org, siteId, userId, numResults: 8 }); if (!cancelled) setItems(scored); } catch (e) { if (!cancelled) setError(e as Error); } })(); return () => { cancelled = true; }; }, [org, siteId, userId]);
if (error) return <EditorialFallbackRail />; if (items === null) return <RailSkeleton />; if (items.length === 0) return <EditorialFallbackRail />;
return ( <div className="recommendations-rail"> {items.map((item) => ( <StoryCard key={item.item_id} item={item} /> ))} </div> );}Checklist before shipping
- You’re passing a stable
user_idper visitor (not a per-page-load random value). - You’re requesting an appropriate
num_resultsfor the surface (don’t fetch 50 to render 4). - You have a fallback rail for empty results.
- You have a fallback rail for network/validation errors.
- Cards render with only the four required fields, even when optional fields are missing.
- Recommendations with
card === nullor missing required fields are skipped (or routed to a placeholder).
Related documentation
- Content Recommendations API Reference — interactive Swagger for the Collector and Recommender APIs.