Dev guideRecipesAPI ReferenceChangelog
Dev guideRecipesUser GuidesNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

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:

PlatformConsiderationsQuery Strategy
WebRich media, SEOFull content, all fields
Mobile appBandwidth, batteryPaginated, mobile image renditions
KioskInteraction designTouch-optimized, high-resolution renditions
SmartwatchScreen size, glanceabilityMinimal text, key data only
VoiceAudio outputText-only, structured for TTS
IoTLimited processingSimple 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


Did this page help you?