Web Setup Guide
Web setup tutorial
Turn any SPA or SSR site into a LinkMe-aware experience. The example below focuses on Next.js (App Router) because it
matches our marketing site, but the same SDK works everywhere.
Before you start
- LinkMe app with an API key (read-only scope is enough for the browser)
- Custom domain or default li-nk.me domain for links
- Modern build tooling (Next.js 13+, Vite, etc.)
1. Install the SDK
npm install @li-nk.me/web-sdk
Package: @li-nk.me/web-sdk
The package is zero-dependency TypeScript that works in any framework—even vanilla JS.
2. Configure environment variables
Expose your app ID (and optional read-only key) to the client:
# .env.local
NEXT_PUBLIC_LINKME_APP_ID=app_123
NEXT_PUBLIC_LINKME_APP_READ_KEY=app_live_read_...
Use public prefixes as required by your framework (e.g., VITE_ for Vite, NEXT_PUBLIC_ for Next.js).
Tip: pass debug: process.env.NODE_ENV !== 'production' to configure() while developing to see URL parsing and
deferred-claim requests logged in the browser console.
3. Bootstrap in Next.js (App Router example)
'use client';
import { ReactNode, useEffect } from 'react';
import { configure, resolveFromUrl, onLink, claimDeferredIfAvailable } from '@li-nk.me/web-sdk';
import { useRouter } from 'next/navigation';
export function LinkMeProvider({ children }: { children: ReactNode }) {
const router = useRouter();
useEffect(() => {
let mounted = true;
let unsubscribe: { remove: () => void } | null = null;
(async () => {
await configure({
baseUrl: 'https://links.yourco.com',
appId: process.env.NEXT_PUBLIC_LINKME_APP_ID!,
appKey: process.env.NEXT_PUBLIC_LINKME_APP_READ_KEY,
});
const initial = await resolveFromUrl();
if (mounted && initial?.path) {
router.replace(initial.path.startsWith('/') ? initial.path : `/${initial.path}`);
} else {
const deferred = await claimDeferredIfAvailable();
if (mounted && deferred?.path) {
router.replace(deferred.path.startsWith('/') ? deferred.path : `/${deferred.path}`);
}
}
unsubscribe = onLink((payload) => {
if (!payload?.path) return;
router.replace(payload.path.startsWith('/') ? payload.path : `/${payload.path}`);
});
})();
return () => {
mounted = false;
unsubscribe?.remove();
};
}, [router]);
return <>{children}</>;
}
Wrap your app layout:
// app/layout.tsx
import { LinkMeProvider } from './LinkMeProvider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<LinkMeProvider>{children}</LinkMeProvider>
</body>
</html>
);
}
4. SPA frameworks (React/Vue/Svelte)
Call the same bootstrap logic from a custom hook or effect:
import { useEffect } from 'react';
import { configure, resolveFromUrl, onLink } from '@li-nk.me/web-sdk';
import { useNavigate } from 'react-router-dom';
export function useLinkMe() {
const navigate = useNavigate();
useEffect(() => {
let cleanup: (() => void) | null = null;
(async () => {
await configure({ appId: import.meta.env.VITE_LINKME_APP_ID });
const initial = await resolveFromUrl();
if (initial?.path) navigate(initial.path);
const sub = onLink((payload) => payload?.path && navigate(payload.path));
cleanup = () => sub.remove();
})();
return () => cleanup?.();
}, [navigate]);
}
5. Test locally
- Run npm run dev, click a LinkMe slug that points to your domain, and confirm the page swaps without a full reload.
- Open the same slug in an incognito window to validate deferred flow (the SDK will call /api/deferred/claim).
- Use your browser's network tab to confirm cid claims return payload JSON.
Next steps
- Review the Web SDK reference for every export (configure, onLink, resolveFromUrl, etc.).
- Pair this with your marketing site or portal by hosting a minimal provider component like the one above.
Svelte
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { configure, resolveFromUrl, onLink } from '@li-nk.me/web-sdk';
onMount(() => {
let active = true;
(async () => {
await configure({
appId: import.meta.env.VITE_LINKME_APP_ID,
appKey: import.meta.env.VITE_LINKME_APP_READ_KEY,
});
const initial = await resolveFromUrl();
if (active && initial?.path) goto(initial.path, { replaceState: true });
})();
const { remove } = onLink((payload) => {
if (payload?.path) goto(payload.path);
});
return () => {
active = false;
remove();
};
});
Angular
import { Injectable, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { configure, onLink, resolveFromUrl } from '@li-nk.me/web-sdk';
@Injectable({ providedIn: 'root' })
export class LinkMeService implements OnDestroy {
private teardown?: () => void;
constructor(private readonly router: Router) {
this.bootstrap();
}
private async bootstrap() {
await configure({
appId: environment.linkMeAppId,
appKey: environment.linkMeAppReadKey,
});
const initial = await resolveFromUrl();
if (initial?.path) {
void this.router.navigateByUrl(initial.path);
}
this.teardown = onLink((payload) => {
if (payload?.path) void this.router.navigateByUrl(payload.path);
}).remove;
}
ngOnDestroy() {
this.teardown?.();
}
}
Inject LinkMeService in your root component (e.g. AppComponent) to activate the SDK.
API Reference
configure(config)
Initialize the SDK with your configuration.
await configure({
appId: '<APP_ID>',
appKey: '<APP_KEY>',
autoResolve: true, // Optional (default: true in browser)
autoListen: true, // Optional (default: true in browser)
stripCid: true, // Optional (default: true)
sendDeviceInfo: true, // Optional (default: true)
resolveUniversalLinks: true, // Optional (default: true)
fetch: customFetch, // Optional (default: globalThis.fetch)
});
resolveFromUrl(url?)
Resolve a link from the current URL (or a provided URL).
const payload = await resolveFromUrl();
// or
const payload = await resolveFromUrl('https://example.com?cid=abc123');
handleLink(url)
Manually resolve an arbitrary URL string.
const payload = await handleLink('https://example.com?cid=abc123');
onLink(callback)
Listen for deep link payloads.
const subscription = onLink((payload: LinkMePayload) => {
console.log('Link received:', payload);
// Navigate based on payload.path
});
// Clean up when done
subscription.remove();
claimDeferredIfAvailable()
Claim a deferred deep link (probabilistic fallback for first launch).
const payload = await claimDeferredIfAvailable();
if (payload?.path) {
router.push(payload.path);
}
track(event, properties?)
Track custom events.
await track('open', { screen: 'home' });
await track('purchase', { amount: 99.99, currency: 'USD' });
setUserId(userId)
Associate a user ID with the session.
setUserId('user-123');
getLastPayload()
Get the most recent resolved payload (if any).
const lastPayload = getLastPayload();
Payload Structure
type LinkMePayload = {
linkId?: string; // Unique link identifier
path?: string; // Deep link path (e.g., "profile")
params?: Record<string, string>; // Query parameters
utm?: Record<string, string>; // UTM parameters
custom?: Record<string, string>; // Custom data
cid?: string; // Click token identifier
duplicate?: boolean; // Whether this was a duplicate claim
};
Configuration options
| Option | Type | Default | Description | | --- | --- | --- | --- | | appId | string | undefined | Optional app
identifier. Sent via x-app-id header. | | appKey | string | undefined | Optional app key. Use a read-only key when
enforced. | | fetch | typeof fetch | globalThis.fetch | Provide your own fetch for environments that need a polyfill. |
| autoResolve | boolean | true in the browser | Resolve window.location.href immediately after configure. | | autoListen
| boolean | true in the browser | Subscribe to popstate/hashchange to pick up navigation changes. | | stripCid | boolean
| true | Removes cid from the address bar after it has been resolved. | | sendDeviceInfo | boolean | true | Sends basic
locale/UA/screen metadata when resolving links. | | resolveUniversalLinks | boolean | true | POSTs to
/api/deeplink/resolve-url when no cid is present but the URL matches your domain. |
Optional: Class-based API
For dependency injection or testing, use the class-based API:
import { LinkMeWebClient } from '@li-nk.me/web-sdk';
const client = new LinkMeWebClient();
await client.configure({
appId: '<APP_ID>',
appKey: '<APP_KEY>',
});
const initial = await client.resolveFromUrl();
const sub = client.onLink((payload) => {});
await client.claimDeferredIfAvailable();
client.setUserId('user-123');
await client.track('open', { screen: 'home' });
sub.remove();
Security notes
- When keys are required, issue a read-only key for browser usage. The SDK only calls read endpoints (GET
/api/deeplink, POST /api/deeplink/resolve-url, POST /api/deferred/claim, POST /api/app-events).
- Consider rate limiting public usage if you expose the SDK on public marketing pages.
- The SDK strips cid parameters from the address bar after they are resolved to avoid leaking tokens via referrers.
Troubleshooting
- Verify cid query parameters make it through your router. If the parameter is removed before configure, pass the full
URL into resolveFromUrl(urlString).
- Use the browser devtools network tab to inspect calls to /api/deeplink and /api/deferred/claim.
- If using SSR frameworks, ensure configure is only called on the client side (use useEffect or similar lifecycle
hooks).