Skip to main content

Deep Linking

Deep linking allows users to open specific StorifyMe stories directly from shared links, providing a seamless experience when content is shared across platforms.

StorifyMe provides two approaches for implementing deep links:

StorifyMe automatically generates and hosts the Apple App Site Association (AASA) file for you. This is the simplest approach and requires no backend infrastructure on your end.

Prerequisites

  • iOS app with StorifyMe SDK integrated
  • StorifyMe account with your app published to the App Store (or TestFlight for testing)

Step 1: Configure App Credentials in Dashboard

  1. Log in to your StorifyMe dashboard

  2. Navigate to Settings → Mobile

  3. In the iOS section, enter:

    • Team ID: Your Apple Developer Team ID (found in Apple Developer Portal)
    • Bundle ID: Your app's bundle identifier (e.g., com.yourcompany.app)
    • App Store ID: Your app's Apple App Store ID (found in App Store Connect)
  4. Click Save

StorifyMe will automatically generate the AASA file at:

  • https://storifyme.xyz/.well-known/apple-app-site-association
  • https://storifyme.com/.well-known/apple-app-site-association

Step 2: Configure Associated Domains in Xcode

  1. Open your Xcode project
  2. Select your app target
  3. Go to the Signing & Capabilities tab
  4. Click + Capability and add Associated Domains
  5. Add the following domains:
    applinks:storifyme.xyz
    applinks:storifyme.com

The deep link URL format is:

https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
https://storifyme.xyz/shorts/{ACCOUNT_ID}/{story_handle}

For iOS 13+ (Using SceneDelegate)

Update your SceneDelegate.swift:

SceneDelegate.swift
import StorifyMe

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return
}

handleDeepLink(url: url)
}

private func handleDeepLink(url: URL) {
// Parse the deep link URL
// Format: https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
let pathComponents = url.pathComponents.filter { $0 != "/" }

guard pathComponents.count >= 3,
(pathComponents[0] == "stories" || pathComponents[0] == "shorts"),
let storyHandle = pathComponents[2] else {
return
}

// Open the story using StorifyMe SDK
StorifyMeInstance.shared.openStoryByHandle(handle: storyHandle)
}
}

For iOS 12 and Earlier (Using AppDelegate)

Update your AppDelegate.swift:

AppDelegate.swift
import StorifyMe

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}

return handleDeepLink(url: url)
}

private func handleDeepLink(url: URL) -> Bool {
let pathComponents = url.pathComponents.filter { $0 != "/" }

guard pathComponents.count >= 3,
(pathComponents[0] == "stories" || pathComponents[0] == "shorts"),
let storyHandle = pathComponents[2] else {
return false
}

StorifyMeInstance.shared.openStoryByHandle(handle: storyHandle)
return true
}
}

Step 4: Smart App Banner (Automatic)

When you configure your App Store ID in the StorifyMe dashboard, we automatically inject a Smart App Banner meta tag into your story web pages. This allows users on iOS Safari to see a banner prompting them to open the story in your app.

The banner appears automatically - no additional configuration needed!

Using Simulator

Test your deep link implementation in the iOS Simulator:

# Test with storifyme.xyz
xcrun simctl openurl booted "https://storifyme.xyz/stories/YOUR_ACCOUNT_ID/test-story"

# Test with storifyme.com
xcrun simctl openurl booted "https://storifyme.com/stories/YOUR_ACCOUNT_ID/test-story"

Replace YOUR_ACCOUNT_ID with your actual StorifyMe account ID.

Using Physical Device

On a physical device:

  1. Send yourself the deep link via Messages or Mail
  2. Tap the link to test the integration
  • App doesn't open: Ensure Associated Domains are properly configured and the app was installed AFTER adding the capability
  • AASA file not loading: Verify your credentials in the StorifyMe dashboard are correct
  • Wrong account: Ensure the URL contains your correct ACCOUNT_ID - StorifyMe uses path-based scoping

Before you begin

Make sure you have configured your sharing domain in the StorifyMe Dashboard Settings. Set either Custom Domain OR Proxy - this domain will be used when links are copied/shared from your stories.

If you want to use your own domain for deep links (e.g., app.yourcompany.com), you'll need to host the AASA file on your own server.

1. Add Associated Domain

In Xcode, add your proxy domain as an associated domain:

  1. Select your app target
  2. Go to Signing & Capabilities
  3. Add Associated Domains capability
  4. Add: applinks:YOUR_PROXY_DOMAIN

Replace YOUR_PROXY_DOMAIN with your custom domain.

2. Host AASA File on Your Server

Create an apple-app-site-association file in your backend at https://YOUR_PROXY_DOMAIN/.well-known/apple-app-site-association:

apple-app-site-association
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.bundleId"],
"paths": ["*"]
}
]
}
}

Replace:

  • TEAMID with your Apple Developer Team ID
  • bundleId with your app's bundle identifier
Finding your Team ID

You can find your Team ID in your Apple Developer account or in Xcode under Signing & Capabilities.

3. Implementation for Custom Domain

For custom domains, update the URL parsing logic to match your domain structure:

Method 1: Direct Story Launch (Recommended)

For shared story links, use openStoryByHandle to open stories directly:

SceneDelegate.swift
import StorifyMe

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return
}

handleDeepLink(url: url)
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
handleDeepLink(url: url)
}

private func handleDeepLink(url: URL) {
guard url.scheme == "https",
let handle = extractStoryHandle(from: url) else {
return
}

// Launch story directly
StorifyMeInstance.shared.openStoryByHandle(handle: handle)
}

private func extractStoryHandle(from url: URL) -> String? {
let pathComponents = url.pathComponents.filter { $0 != "/" }

switch pathComponents.count {
case 1:
// For URLs like: https://domain.com/story-handle
return pathComponents[0]

case 2 where pathComponents[0] == "story":
// For URLs like: https://domain.com/story/story-handle
return pathComponents[1]

case 2 where pathComponents[0] == "stories":
// For URLs like: https://domain.com/stories/story-handle
return pathComponents[1]

default:
return nil
}
}
}

Method 2: Widget-Based Launch

If you need to open a story within a loaded widget context:

ViewController.swift
import StorifyMe

class ViewController: UIViewController {
@IBOutlet weak var storifyMeWidget: StorifyMeWidget!
private var pendingHandle: String?

override func viewDidLoad() {
super.viewDidLoad()
setupStorifyMeWidget()
}

private func setupStorifyMeWidget() {
storifyMeWidget.eventHandler = self
storifyMeWidget.setWidgetId(widgetId: YOUR_WIDGET_ID)
storifyMeWidget.load()
}

func handleDeepLink(handle: String) {
if storifyMeWidget.isLoaded {
// Widget is already loaded, open immediately
openStoryInWidget(handle: handle)
} else {
// Store handle to open after widget loads
pendingHandle = handle
}
}

private func openStoryInWidget(handle: String) {
storifyMeWidget.openWidgetStoryByHandle(handle) { result in
switch result {
case .success:
// Story opened successfully
break
case .storyNotFound:
// Handle not found in this widget
// Fall back to direct launch
StorifyMeInstance.shared.openStoryByHandle(handle: handle)
case .widgetNotLoaded:
// Widget not ready yet
break
}
}
}
}

extension ViewController: StorifyMeStoryEventProtocol {
func onLoad(widgetId: Int, storyList: [StorifyMeStory]) {
// Widget loaded, check if we have a pending handle
if let handle = pendingHandle {
openStoryInWidget(handle: handle)
pendingHandle = nil
}
}

func onFail(widgetId: Int, error: String) {
// If widget fails to load but we have a handle, try direct launch
if let handle = pendingHandle {
StorifyMeInstance.shared.openStoryByHandle(handle: handle)
pendingHandle = nil
}
}
}

UIKit Integration

For UIKit-based apps, handle deep links in your AppDelegate:

AppDelegate.swift
import StorifyMe

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}

return handleDeepLink(url: url)
}

private func handleDeepLink(url: URL) -> Bool {
guard let handle = extractStoryHandle(from: url) else {
return false
}

// Launch story directly
StorifyMeInstance.shared.openStoryByHandle(handle: handle)
return true
}

private func extractStoryHandle(from url: URL) -> String? {
let pathComponents = url.pathComponents.filter { $0 != "/" }

switch pathComponents.count {
case 1:
return pathComponents[0]
case 2 where pathComponents[0] == "story":
return pathComponents[1]
case 2 where pathComponents[0] == "stories":
return pathComponents[1]
default:
return nil
}
}
}

SwiftUI Integration

For SwiftUI apps, handle deep links using onOpenURL:

ContentView.swift
import SwiftUI
import StorifyMe

struct ContentView: View {
var body: some View {
// Your main content
VStack {
// StorifyMe widget or other content
}
.onOpenURL { url in
handleDeepLink(url: url)
}
}

private func handleDeepLink(url: URL) {
guard let handle = extractStoryHandle(from: url) else {
return
}

StorifyMeInstance.shared.openStoryByHandle(handle: handle)
}

private func extractStoryHandle(from url: URL) -> String? {
let pathComponents = url.pathComponents.filter { $0 != "/" }

switch pathComponents.count {
case 1:
return pathComponents[0]
case 2 where pathComponents[0] == "story":
return pathComponents[1]
case 2 where pathComponents[0] == "stories":
return pathComponents[1]
default:
return nil
}
}
}

URL Pattern Examples

Your implementation should handle various URL patterns depending on your backend setup:

URL PatternHandle Extraction
https://domain.com/story-handlepathComponents[0]
https://domain.com/story/story-handlepathComponents[1]
https://domain.com/stories/story-handlepathComponents[1]
https://domain.com/content/story-handlepathComponents[1]

Using Simulator

Test your deep link implementation in the iOS Simulator:

  1. Open Safari in the simulator
  2. Navigate to your deep link URL
  3. Tap the link to trigger your app

Using Device

On a physical device:

  1. Send yourself the deep link via Messages or Mail
  2. Tap the link to test the integration
  3. Or use Safari and navigate to the URL directly

Using Xcode

You can also test using Xcode's URL scheme testing:

  1. In Xcode, go to ProductSchemeEdit Scheme
  2. Select Run from the left panel
  3. Go to Arguments tab
  4. Add your deep link URL under Arguments Passed On Launch

Best Practices

  1. Use Direct Launch for Shared Content: openStoryByHandle is more reliable for shared links as it doesn't depend on widget loading
  2. Implement Fallback Logic: If a story isn't found in a widget, fall back to direct launch
  3. Handle Multiple URL Patterns: Make your URL parsing robust to handle different backend configurations
  4. Validate URLs: Check if the URL belongs to your domain before processing
  5. User Experience: Show loading states while processing deep links
  6. Error Handling: Gracefully handle invalid or expired story handles

Common Issues

  • Verify your apple-app-site-association file is accessible and properly formatted
  • Check that your Team ID and bundle identifier are correct
  • Ensure the domain in Associated Domains matches your proxy domain exactly
  • Make sure the file is served with Content-Type: application/json

Story Not Opening

  • Verify the story handle exists and is published
  • Check if the story is available in the widget you're trying to open it from
  • Use direct launch (openStoryByHandle) as a fallback

URL Parsing Issues

  • Test with different URL patterns your backend might generate
  • Add logging to debug which path components contain the handle
  • Handle edge cases like URLs with query parameters or fragments

Associated Domain Verification