0

10 min left

0% complete

Blog

How to Build a Seamless Social Sharing Experience with Open Graph Previews and Deep Linking

Imagine sharing a product link and seeing a rich preview with an image, title, and description—just like the big apps do. Even better: when someone taps it, your app opens directly to that item, not t

Imagine sharing a product link and seeing a rich preview with an image, title, and description—just like the big apps do. Even better: when someone taps it, your app opens directly to that item, not the home screen. Sounds smooth, right? That’s exactly what Open Graph (OG) previews and deep linking make possible.

In this guide, we’ll walk through how to implement a full-featured social sharing system that:

  • Generates rich previews on platforms like X (formerly Twitter), WhatsApp, and Facebook
  • Opens your app seamlessly via Universal Links (iOS) or custom URL schemes
  • Handles tricky edge cases like in-app browsers and missing app installations
  • Delivers a polished experience across devices and platforms

Let’s break it down step by step.


Why This Matters: The Power of Rich Sharing

Plain URLs are forgettable. When you share one in a chat or post, it’s just text—no image, no branding, no context. That leads to lower engagement.

By contrast, Open Graph tags tell social platforms how to display your link visually. Combine that with deep linking, and you create a frictionless journey: tap a beautiful preview → open the app → see the exact content. This boosts click-through rates and improves user retention.

We’re going to build this from the ground up—including how to handle real-world complications like blocked deep links and server-side rendering issues.


How It Works: The Big Picture

At its core, this system involves two key components:

  1. Open Graph (OG) Previews – A special server-rendered HTML page with <meta> tags that social crawlers read to generate rich previews.
  2. Deep Linking – Directing tapped links into your app using Universal Links (iOS) or custom URL schemes (myapp://).

Here’s the flow:

  1. A user shares an item link:

https://your-domain.com/api/og/item/abc123?scheme=yourapp-dev

  1. A social crawler (like X or Facebook) fetches the URL and parses OG meta tags.
  2. A rich preview card is displayed with an image, title, and description.
  3. When someone taps the link:

- On Safari/Chrome: Opens the app directly via Universal Links (if installed)

- In in-app browsers (X, Instagram): Loads a landing page with fallback options

  1. The app opens to the correct screen with proper navigation history.

Now let’s dive into each layer.


Setting Up Your Environment & Domains

If you're using Expo for deployment, you’ll need to manage different domains per environment. Expo uses a double-hyphen convention for non-production builds:

EnvironmentDomainDeploy Command
Productionyour-app.expo.appnpm run deploy:prod
Previewyour-app--preview.expo.appnpm run deploy:preview
Developmentyour-app--dev.expo.appnpm run deploy:dev
🔍 Note: It's -- (double hyphen), not -. This is fixed in Expo and can't be changed unless you use a custom domain.

Each environment needs the correct base URL set:

# Development
EXPO_PUBLIC_APP_ENV=development
EXPO_PUBLIC_BASE_URL=https://your-app--dev.expo.app

# Preview  
EXPO_PUBLIC_APP_ENV=preview
EXPO_PUBLIC_BASE_URL=https://your-app--preview.expo.app

# Production
EXPO_PUBLIC_APP_ENV=production
EXPO_PUBLIC_BASE_URL=https://your-app.expo.app

These values are used both client-side and server-side to generate correct URLs.


Deep Linking on iOS: Universal Links

Custom schemes like myapp://item/123 work, but they have limitations—they won't open from many in-app browsers and lack trust. Enter Universal Links.

What Are Universal Links?

Universal Links are HTTPS links that iOS can route directly to your app if it's installed. They:

  • Work in Safari (with a system banner prompting app open)
  • Are more secure—Apple verifies ownership via a hosted JSON file
  • Don’t require the app to be opened through a redirect

Setup Requirements

To enable Universal Links, two things must happen:

1. Configure Associated Domains in app.config.ts

ios: {
  associatedDomains: [`applinks:your-app--dev.expo.app`]
}

Do this for each environment:

  • applinks:your-app.expo.app (production)
  • applinks:your-app--preview.expo.app (preview)
  • applinks:your-app--dev.expo.app (development)

2. Serve the AASA File

Create this file at public/.well-known/apple-app-site-association:

{
  "applinks": {
    "details": [
      {
        "appID": "YOUR_TEAM_ID.com.yourcompany.yourapp",
        "paths": ["/item/*", "/api/og/item/*"]
      },
      {
        "appID": "YOUR_TEAM_ID.com.yourcompany.yourapp.preview",
        "paths": ["/item/*", "/api/og/item/*"]
      },
      {
        "appID": "YOUR_TEAM_ID.com.yourcompany.yourapp.dev",
        "paths": ["/item/*", "/api/og/item/*"]
      }
    ]
  }
}
⚠️ Important:
  • Filename must be exactly apple-app-site-association (no .json extension)
  • Must be served over HTTPS
  • iOS caches this file for ~24 hours—reinstall the app to refresh

3. Conditions for Success

  • App must be installed via TestFlight or App Store (not Expo Go)
  • Only works in Safari, not in WebViews (X, Instagram, etc.)
  • Device must have internet to verify AASA

Handling Deep Links in the App

Even when a link opens your app, there’s a UX problem: no navigation history.

If the user opens via yourapp://item/123, they land on the item screen—but pressing “back” does nothing. They’re stuck.

The Solution: Reset Navigation with useDeepLinkHandler

We solve this with a custom hook that resets the navigation stack to [Home] → [Item].

Here’s how it works:

// Located at shared/hooks/useDeepLinkHandler.ts
export function useDeepLinkHandler(navigationRef) {
  // Listens for incoming URLs (cold/warm start)
  // Extracts item ID from patterns:
  // - https://.../api/og/item/123
  // - yourapp://item/123

  // Then resets navigation:
  navigateToItem(navigationRef, itemId); // [Home] -> [Item]
}

Used in your root layout:

const navigationRef = useNavigationContainerRef();
useDeepLinkHandler(navigationRef);

This ensures users can always go back to home—no more trapped screens.


Server-Side Rendering: Avoiding Common Pitfalls

Your Open Graph endpoint runs on the server, so you can’t use React Native imports. If you try:

import { Dimensions, Platform } from "react-native"; // ❌ Breaks server

You’ll get: Cannot use 'import.meta' outside a module.

Fix: Use server-constants.ts

Create a server-safe version of your config:

// shared/utils/server-constants.ts
export const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV!;
export const APP_SCHEME = getSchemeForEnv(APP_ENV) + '://';
export const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
// ... other env vars

Now follow this rule:

File TypeImport From
React components@/shared/utils/constants
API routes (+api.ts)@/shared/utils/server-constants

Building the OG Landing Page: Fallbacks for In-App Browsers

When users tap your link inside X, Instagram, or Snapchat, it opens in a WebView. These block both Universal Links and custom schemes.

So instead of failing silently, we show a smart landing page.

Layout: Two Clear Options

┌─────────────────────────────┐
│                             │
│      Cool Sunglasses        │
│   Open Chosen to view...    │
│                             │
│    ┌─────────────────┐      │
│    │  View in App    │      │  ← Tries deep link
│    └─────────────────┘      │
│                             │
│           — or —            │
│                             │
│    ┌─────────────────┐      │
│    │   Copy Link     │      │  ← Copies URL + hint
│    └─────────────────┘      │
│                             │
│  Paste in Safari to open    │
│                             │
└─────────────────────────────┘

The “Copy Link” button:

  • Uses Web Share API (if supported)
  • Falls back to clipboard copy
  • Shows visual feedback: “Copied!” in green
  • Displays hint: “Paste in Safari to open the app”

This gives users control—and keeps them moving forward.


Enhancing UX: Smart App Install Detection

What if the app isn't installed? Previously, tapping “View in App” did nothing.

Now, we detect failure and show download options.

How It Works

  1. User taps “View in App”
  2. JavaScript tries to open yourapp://item/123
  3. After 2.5 seconds (and page still visible), we assume the app isn’t installed
  4. Show App Store / Google Play buttons
┌─────────────────────────────┐
│                             │
│   App not installed?        │
│   Download it now:          │
│                             │
│    ┌─────────────────┐      │
│    │ 📱 App Store    │      │
│    └─────────────────┘      │
│                             │
│    ┌─────────────────┐      │
│    │ 🤖 Google Play  │      │
│    └─────────────────┘      │
│                             │
│      already installed?     │
│                             │
│    ┌─────────────────┐      │
│    │   Try Again     │      │
│    └─────────────────┘      │
│                             │
└─────────────────────────────┘
  • User-agent detection shows only relevant store
  • “Try Again” reattempts the deep link

No guesswork. Clear next steps.


Better Navigation: Modal-Based Deep Link Flow

Old problem: When opening /item/123, Expo Router would show a "Page Not Found" flash before redirecting.

New Approach: Open a Modal Instead

Instead of navigating to a route, we:

  1. Set a deep link item ID flag
  2. Open the item detail as a modal
  3. Navigate behind it to / (home)
// In useDeepLinkHandler.ts
modalStore.open('itemDetail', { itemId });
router.replace('/');

Benefits:

  • No “Page Not Found” flash
  • User sees home screen under the modal
  • Swipe-to-dismiss or back button closes modal → lands on home
  • Works on cold start and warm start

We store the deep-linked item ID in a Zustand store (useModalStore) to survive early mounting.


Multi-Format Sharing: Link, Image, and Save

Users want to share in different ways:

  • As a link → rich preview appears
  • As an image → branded card with details
  • To save locally → save to gallery

We abstract all this in the useShareItem hook.

Features of useShareItem

interface UseShareItemReturn {
  shareRef: React.RefObject<View>;
  shareUrl: string;
  getShareText: () => string;
  shareAsImage: () => Promise<void>;
  saveToGallery: () => Promise<void>;
  shareNative: () => Promise<void>;
  openShareModal: () => void;
  isSharing: boolean;
  isSaving: boolean;
}

Share Text Example

🛍️ Cool Sunglasses
🔥 30% off
👤 Recommended by Ahmed
🏪 Fashion Store

📲 View on Chosen
https://your-app.expo.app/api/og/item/abc123

How Image Sharing Works

  1. react-native-view-shot captures a hidden <View ref={shareRef}>
  2. Exports as PNG
  3. Shared via expo-sharing or saved with expo-media-library

All permissions are scoped—users only grant write access, not full library access.


Testing Your Setup

✅ Safari (Universal Links)

https://your-app--dev.expo.app/api/og/item/abc123

👉 App opens directly (if installed via TestFlight)

✅ Custom Scheme

yourapp-dev://item/abc123

👉 Opens app with back navigation to home

✅ In-App Browser Test

  1. Share a link in X
  2. Tap the preview
  3. See landing page with:

- “View in App” button

- “Copy Link” button → copies + shows hint

  1. Paste in Safari → app opens

Troubleshooting Common Issues

“Page Not Found” on Deep Link

Cause: Expo Router tries to render the route before handler runs.

Fix: Use CommonActions.reset() in useDeepLinkHandler to bypass intermediate screens.

OG Preview Shows App Name Instead of Item

Cause: Missing API environment variables on server

Fix: Set EXPO_PUBLIC_API_PUBLIC_TEST_URL in EAS secrets or Expo dashboard

Universal Links Not Working

Check:

  • App installed via TestFlight? (Not Expo Go)
  • AASA file accessible at /.well-known/apple-app-site-association?
  • associatedDomains in app.config.ts matches domain?
  • Tested in Safari? (Not in-app browser)
  • Reinstalled app? (iOS caches AASA)

Cannot use import.meta outside a module

Cause: API route imported from constants.ts with React Native modules

Fix: Import from server-constants.ts instead


Key Files Reference

FilePurpose
app/api/og/item/[id]+api.tsOG HTML generation + landing page + app store links
app/api/og/item/[id]/image+api.tsImage proxy for X compatibility
public/.well-known/apple-app-site-associationiOS Universal Links config
shared/hooks/useDeepLinkHandler.tsHandles deep links with modal support
shared/hooks/useShareItem.tsUnified sharing logic
shared/store/useModalStore.tsManages modal state and deep link item
components/ShareItemModal.tsxShare options UI
components/ShareableItemCard.tsxBranded image card for sharing
shared/utils/constants.tsClient-side constants
shared/utils/server-constants.tsServer-safe constants
app.config.tsEnvironment-specific associatedDomains

Learnings & Takeaways

After building this system, here are the key lessons:

  • Universal Links require strict setup – Right domain, AASA file, TestFlight install.
  • In-app browsers block deep links – Always provide a fallback landing page.
  • Server-side rendering needs clean separation – Never import React Native in API routes.
  • User experience matters at every touchpoint – From preview design to back-button behavior.
  • Modals improve navigation flow – Avoiding flash and dead ends.

With this architecture, you can deliver a polished, professional sharing experience that matches top-tier apps—all while handling real-world edge cases gracefully.


Further Reading

Contents