10 min left
0% complete
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:
- Open Graph (OG) Previews – A special server-rendered HTML page with
<meta>tags that social crawlers read to generate rich previews. - Deep Linking – Directing tapped links into your app using Universal Links (iOS) or custom URL schemes (
myapp://).
Here’s the flow:
- A user shares an item link:
https://your-domain.com/api/og/item/abc123?scheme=yourapp-dev
- A social crawler (like X or Facebook) fetches the URL and parses OG meta tags.
- A rich preview card is displayed with an image, title, and description.
- 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
- 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:
| Environment | Domain | Deploy Command |
|---|---|---|
| Production | your-app.expo.app | npm run deploy:prod |
| Preview | your-app--preview.expo.app | npm run deploy:preview |
| Development | your-app--dev.expo.app | npm 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.appThese 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.jsonextension) - 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 serverYou’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 varsNow follow this rule:
| File Type | Import 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
- User taps “View in App”
- JavaScript tries to open
yourapp://item/123 - After 2.5 seconds (and page still visible), we assume the app isn’t installed
- 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:
- Set a deep link item ID flag
- Open the item detail as a modal
- 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/abc123How Image Sharing Works
react-native-view-shotcaptures a hidden<View ref={shareRef}>- Exports as PNG
- Shared via
expo-sharingor saved withexpo-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
- Share a link in X
- Tap the preview
- See landing page with:
- “View in App” button
- “Copy Link” button → copies + shows hint
- 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? associatedDomainsinapp.config.tsmatches 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
| File | Purpose |
|---|---|
app/api/og/item/[id]+api.ts | OG HTML generation + landing page + app store links |
app/api/og/item/[id]/image+api.ts | Image proxy for X compatibility |
public/.well-known/apple-app-site-association | iOS Universal Links config |
shared/hooks/useDeepLinkHandler.ts | Handles deep links with modal support |
shared/hooks/useShareItem.ts | Unified sharing logic |
shared/store/useModalStore.ts | Manages modal state and deep link item |
components/ShareItemModal.tsx | Share options UI |
components/ShareableItemCard.tsx | Branded image card for sharing |
shared/utils/constants.ts | Client-side constants |
shared/utils/server-constants.ts | Server-safe constants |
app.config.ts | Environment-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.