Omnichannel content delivery
Deliver structured content through Optimizely Graph to multiple platforms including web, mobile apps, kiosks, and IoT devices from a single content source.
Omnichannel delivery distributes content from a single source (Optimizely Graph) to multiple platforms and devices. This approach ensures content consistency while adapting presentation for each channel's unique requirements.
This guide focuses on platform-specific strategies: understanding the differences between web, mobile, kiosk, voice, and IoT platforms, and how to optimize queries for each. For framework implementation code, see Dynamic frontend integration.
Why omnichannel delivery?
Modern digital experiences span multiple touchpoints:
- Web – Desktop and mobile browsers
- Mobile apps – iOS and Android native applications
- Kiosks – Interactive displays in retail, hospitality, and healthcare
- Smartwatches – Wearable device interfaces
- Voice assistants – Alexa, Google Assistant, Siri
- IoT devices – Smart displays, digital signage, connected products
Optimizely Graph provides a platform-agnostic content API that delivers structured content to all these channels from a single content repository.
Core principles
1. Create once, publish everywhere
Authors create content once in the CMS. Graph distributes it to all channels automatically.
Benefits:
- Consistent messaging across channels
- Reduced authoring effort
- Single source of truth
- Synchronized content updates
2. Adaptive delivery
Each channel requests only the fields and formats it needs.
Example: Same article, different channels:
# Web: Full article with rich media
query WebArticle($key: String!, $locale: [Locales]) {
Article(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
title
body
author { name, avatar }
publishedDate
heroImage { desktopUrl, alt, width, height }
relatedArticles { title, excerpt, image { thumbnailUrl } }
tags { tag }
}
}
}
# Mobile app: Optimized for smaller screens
query MobileArticle($key: String!, $locale: [Locales]) {
Article(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
title
excerpt
publishedDate
thumbnailImage: heroImage { mobileUrl }
}
}
}
# Smartwatch: Minimal data
query WatchArticle($key: String!, $locale: [Locales]) {
Article(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
title
excerpt
}
}
}
# Voice assistant: Text only
query VoiceArticle($key: String!, $locale: [Locales]) {
Article(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
title
textOnlyBody
}
}
}3. Platform-specific optimization
Optimize queries for each platform's constraints:
| Platform | Considerations | Query Strategy |
|---|---|---|
| Web | Rich media, SEO | Full content, all fields |
| Mobile app | Bandwidth, battery | Paginated, mobile image renditions |
| Kiosk | Interaction design | Touch-optimized, high-resolution renditions |
| Smartwatch | Screen size, glanceability | Minimal text, key data only |
| Voice | Audio output | Text-only, structured for TTS |
| IoT | Limited processing | Simple data, small payloads |
Platform-specific implementations
For complete framework integration examples (React, Vue, Angular, Next.js), see Dynamic frontend integration. This section focuses on platform-specific considerations and query patterns.
Web applications
Characteristics:
- Full-featured content with rich media
- SEO requirements
- Multiple device sizes (desktop, tablet, mobile)
- Fast network connections (typically)
Query strategy:
# Web: Request full content with SEO metadata
query WebPageContent($slug: String!, $locale: [Locales]) {
Page(
where: { urlSlug: { eq: $slug } }
locale: $locale
) {
item {
heading
fullContent
metadata {
title
description
keywords
ogImage
}
heroImage {
desktopUrl
tabletUrl
mobileUrl
alt
width
height
}
components {
... on TextBlock { text }
... on ImageGallery { images { desktopUrl, alt, caption } }
... on VideoEmbed { videoUrl, thumbnail }
}
}
}
}Implementation: See Headless architecture patterns for SSG/SSR/CSR patterns and Dynamic frontend integration for framework setup.
Mobile applications
Characteristics:
- Limited bandwidth (cellular networks)
- Smaller screens
- Touch interfaces
- Battery constraints
- Offline support needs
Query strategy:
# Mobile: Optimized payload, selecting the mobile image rendition
query MobileProducts($limit: Int!, $cursor: String) {
Product(
limit: $limit
cursor: $cursor
orderBy: { featured: DESC }
) {
items {
id
name
price
summary # Use summary instead of full description
thumbnail: image {
mobileUrl # Pre-generated rendition sized for mobile screens
}
}
cursor
total
}
}Key differences from web:
- Select the mobile image URL field (
mobileUrl) instead of the desktop one - Use summaries instead of full content
- Implement pagination/infinite scroll
- Cache aggressively for offline support
Implementation: See Dynamic frontend integration for React Native, iOS (Swift), and Android (Kotlin) integration patterns.
Native mobile apps (iOS/Android)
For native iOS (Swift) and Android (Kotlin) integration code examples, see Dynamic frontend integration.
Platform-specific considerations:
- iOS: Use URLSession for GraphQL requests, select a high-density image URL field for Retina displays
- Android: Use OkHttp or Retrofit, implement RecyclerView with pagination
- Both: Implement offline caching, handle low-bandwidth scenarios, optimize battery usage
Digital kiosks
Characteristics:
- Large touchscreens (1080p or 4K)
- Public/shared devices
- Standalone operation (may be offline)
- Accessibility requirements (ADA compliance)
- Location-specific content
Query strategy:
# Kiosk: High-resolution image rendition, location-specific content
query GetKioskContent($location: String!) {
KioskContent(
where: { location: { eq: $location } }
) {
items {
title
description
largeImage: image {
kioskUrl # Full HD rendition for large touchscreens
}
interactiveElements {
type
label
action
}
accessibilityText # Required for ADA compliance
}
}
}Key considerations:
- Poll for content updates (every 30-60 seconds)
- Large touch targets (minimum 44x44px)
- High-resolution images (1920x1080 or higher)
- Implement idle timeouts and reset to home screen
- Offline mode for network outages
- Screen reader support
Voice assistants
Characteristics:
- Audio-only output
- Conversational interaction
- No visual elements
- Context switching between topics
Query strategy:
# Voice: Text-only, optimized for speech synthesis
query GetArticleForVoice($key: String!, $locale: [Locales]) {
Article(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
title
summaryText # Short, spoken-friendly summary
audioTranscript # Pre-written script optimized for TTS
}
}
}Key considerations:
- Return only text content (no images, videos)
- Keep responses concise (30-60 seconds of speech)
- Use natural, conversational language
- Structure for text-to-speech (avoid special characters, format numbers as words)
- Provide conversation flow cues
Adaptive image delivery
Optimizely Graph does not resize or transcode images – it returns the values indexed from your content model. To deliver optimized images per platform, model each rendition as its own URL field (generated by the CMS, a DAM, or an image CDN) and let each channel select the field it needs:
# Illustrative content model
type Image {
alt: String
mobileUrl: String # 400px wide, WebP
tabletUrl: String # 800px wide
desktopUrl: String # 1200px wide
kioskUrl: String # 1920x1080
thumbnailUrl: String # 200px square
}# Web - request every rendition needed to build a srcset
query WebImages {
Page {
items {
heroImage {
alt
mobileUrl
tabletUrl
desktopUrl
}
}
}
}
# Mobile - single rendition, smallest payload
query MobileImages {
Page {
items {
heroImage {
alt
mobileUrl
}
}
}
}
# Kiosk - high-resolution rendition
query KioskImages {
Page {
items {
heroImage {
alt
kioskUrl
}
}
}
}Because the rendition URLs are plain indexed fields, each channel transfers only the URLs it will actually render, and the same query works for any storage or CDN behind the content.
Content variation by channel
Store channel-specific content variations:
query GetContentVariations($key: String!, $locale: [Locales]) {
Content(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
# Shared content
title
baseContent
# Channel-specific variations
webContent
mobileContent
kioskContent
voiceContent
# Or use structured variations
variations {
channel
content
metadata
}
}
}
}Usage in application:
function getContentForChannel(content, channel) {
// Return channel-specific variation or fall back to base content
return content[`${channel}Content`] || content.baseContent;
}
const content = data.Content.item;
const displayContent = getContentForChannel(content, 'mobile');Synchronization strategies
Real-time sync
Keep content synchronized across all channels by fanning out each content change to every platform. getChangedContentIds returns the IDs of the content that changed – see Manage webhooks for how to receive those change notifications from Optimizely Graph.
// Propagate updates to all channels
const contentIds = await getChangedContentIds();
await Promise.all(
contentIds.flatMap((contentId) => [
invalidateWebCache(contentId),
notifyMobileApps(contentId),
updateKioskDisplays(contentId),
refreshIoTDevices(contentId)
])
);Offline support
Some mobile and kiosk applications need offline access – for example, apps used in areas with unreliable connectivity or kiosks that must keep running through network outages. If yours does, apply the strategy below. For implementation details, see Dynamic frontend integration.
Strategy:
- Cache essential content locally
- Implement a queue for actions performed while offline
- Sync when the connection is restored
- Show offline indicators to users
Best practices
1. Design for the smallest screen first
Start with the most constrained platform (smartwatch, voice), then expand for larger platforms.
2. Use feature detection
Query for platform capabilities:
function getOptimalQuery(platform) {
// Build the selection set from the platform's capabilities
const fields = ['_metadata { key }', 'title'];
if (platform.supportsImages) {
fields.push('image { url }');
}
if (platform.supportsVideo) {
fields.push('video { url }');
}
return `
query PlatformContent($key: String!, $locale: [Locales]) {
Content(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
${fields.join('\n ')}
}
}
}
`;
}3. Implement graceful degradation
function renderContent(content, platform) {
if (platform === 'web' && content.richText) {
return <RichTextRenderer content={content.richText} />;
}
if (platform === 'mobile' && content.markdown) {
return <MarkdownRenderer content={content.markdown} />;
}
// Fallback to plain text
return <TextRenderer content={content.plainText} />;
}4. Monitor per-channel performance
Track metrics for each platform:
analytics.track('content_loaded', {
platform: 'mobile',
contentType: 'article',
loadTime: performance.now() - startTime,
dataSize: response.headers.get('content-length')
});5. Optimize for bandwidth
Mobile and IoT devices often have limited bandwidth:
// Request minimal data for mobile
const MOBILE_QUERY = gql`
query MobileContent($key: String!, $locale: [Locales]) {
Content(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
item {
_metadata { key }
title
summary # Use summary instead of full body
thumbnailImage: image { mobileUrl }
}
}
}
`;Testing across channels
Cross-platform testing strategy
describe('Omnichannel content delivery', () => {
const platforms = ['web', 'mobile', 'kiosk', 'voice'];
platforms.forEach(platform => {
test(`delivers content for ${platform}`, async () => {
const query = getPlatformQuery(platform);
const response = await fetchFromGraph(query);
expect(response).toHaveValidStructure(platform);
expect(response.dataSize).toBeLessThan(
MAX_PAYLOAD_SIZE[platform]
);
});
});
});Common pitfalls
1. Over-fetching on mobile
Don't request web-sized data for mobile devices:
// ❌ Bad: Same query for all platforms
const query = `{ Content { items { heroImage { desktopUrl } fullBody } } }`;
// ✅ Good: Platform-specific queries
const mobileQuery = `{ Content { items { heroImage { mobileUrl } summary } } }`;2. Ignoring offline scenarios
If your mobile or kiosk app runs where connectivity is unreliable, do not assume the network is always available – add an offline fallback:
// ✅ Good: Implement offline fallback
const { data, loading, error } = useQuery(GET_CONTENT, {
fetchPolicy: 'cache-first',
errorPolicy: 'all'
});3. Inconsistent content models
Ensure all channels can access required fields:
# ✅ Good: Shared base fields with optional channel-specific fields
type Article {
id: String! # Required for all channels
title: String! # Required for all channels
summary: String! # Required for all channels
fullBody: String # Optional - web only
audioScript: String # Optional - voice only
}Next steps
- Implement Dynamic frontend integration for specific frameworks
- Learn Headless architecture patterns for each platform type
- Review Caching best practices for multi-channel scenarios
Updated 4 days ago
