@dl2app/web
v0.1.1
Published
DL2App for the web: smart app banner, link resolution, events and a UTM helper. No dependencies, no cookies.
Readme
@dl2app/web
DL2 for websites: a smart app banner, link resolution, events and a helper that carries UTM parameters into your app links.
- No dependencies. About 5.4 KB minified.
- No cookies. The only thing stored is one
localStoragekey that remembers a dismissed banner. - Nothing throws: every function is safe to call on any page, including during server rendering.
Install
npm install @dl2app/webSet up
import DL2 from "@dl2app/web";
DL2.init({
key: "pk_live_xxx", // publishable key from the dashboard
host: "myapp.dl2.app", // your link host
});
DL2.banner({
appName: "My App",
icon: "https://www.myapp.com/app-icon.png",
link: "welcome", // slug or path the "Open" button opens
});Or in one call: DL2.init({ key, host, banner: { appName: "My App", link: "welcome" } }).
init options:
| Option | Default | |
| --- | --- | --- |
| key | required | Publishable key. It must start with pk_live_. |
| host | required | Your link host. |
| altHost | host with -alt added to the first label | Set it when host is a custom domain: altHost: "myapp-alt.dl2.app". |
| apiBaseUrl | https://api.dl2.app | |
| banner | none | Show the banner straight away. |
API
banner(options) returns the element, or null when nothing is shown
| Option | Default | |
| --- | --- | --- |
| appName | required | Shown in the banner. |
| icon | none | URL of the app icon. |
| link | the link host root | Slug or path to open, for example "welcome" or "/product/42". |
| text | "Open in the app" | Second line. |
| cta | "Open" | Button label. |
| position | "top" | "top" or "bottom". |
| dismissDays | 14 | Days a dismissed banner stays hidden. |
| qr | false | On desktop, show a card with a QR code of link. |
| nonce | none | CSP nonce to put on the banner's <style> element. |
- Phones and tablets get a bar with the app name, icon and an "Open" button. The button points at your alternate host (
https://myapp-alt.dl2.app/welcome). A tap that crosses to another domain is what lets iOS and Android open the app; if the app is not installed, DL2 sends the visitor to the store. The page's UTM parameters are added to the link. - Desktop shows nothing unless
qr: trueandlinkare set. Then it shows a small card with the QR imagehttps://myapp.dl2.app/_/qr/welcome.svgto scan with a phone. - Dismissing is remembered in
localStorage(dl2_banner_dismissed). If storage is blocked, the banner simply returns on the next page. - Accessible: a labelled region, a real link and a real button with a "Dismiss" label, 44 px touch targets, visible keyboard focus.
- App name and text are inserted as text, never as HTML.
The banner is fixed over the page. To keep it from covering a fixed header, add padding yourself when it shows:
if (DL2.banner({ appName: "My App" })) document.body.style.paddingTop = "60px";Theme
Set CSS variables on #dl2-banner or any ancestor. Light and dark defaults follow the visitor's system setting.
#dl2-banner {
--dl2-bg: #ffffff; /* background */
--dl2-fg: #111111; /* app name */
--dl2-muted: #666666; /* second line and close button */
--dl2-accent: #0a66ff; /* Open button */
--dl2-accent-fg: #ffffff; /* Open button text */
--dl2-radius: 22px; /* button and card corners */
--dl2-font: system-ui, sans-serif;
}resolve(url) returns Promise<{ path, data, url, link_id, utm } | null>
Resolves a DL2 link without redirecting, for example to render the right content on a web fallback page. null when the link does not exist or the request fails.
track(name, { value?, currency?, props? }) returns Promise<boolean>
Records an event. The ID sent with it is random, kept in memory only and new on every page load, so it does not identify a visitor across pages.
withUtm(link) returns string
Copies utm_source, utm_medium, utm_campaign, utm_term and utm_content from the current page onto a link, so a campaign survives the hop into the app. Parameters the link already has are kept.
// Page: https://www.myapp.com/sale?utm_source=newsletter&utm_campaign=spring
DL2.withUtm("https://myapp.dl2.app/sale");
// "https://myapp.dl2.app/sale?utm_source=newsletter&utm_campaign=spring"Helpers
altHostOf(host), isMobile(userAgent, maxTouchPoints), openUrl(altHost, link), qrUrl(host, link), isDismissed(days) and dismiss() are exported for custom banners.
Troubleshooting
The "Open" button goes to the store although the app is installed
The alternate host must be in the app's configuration:
applinks:myapp-alt.dl2.appon iOS and a verified intent filter for it on Android. The Expo plugin of@dl2app/react-nativeadds both.Check what the platforms see:
curl -i https://myapp-alt.dl2.app/.well-known/apple-app-site-association curl https://app-site-association.cdn-apple.com/a/v1/myapp-alt.dl2.app curl -i https://myapp-alt.dl2.app/.well-known/assetlinks.jsonIf your website is on the same domain as your link host, use a different
altHost: a tap within one domain never opens the app on iOS.iOS remembers when a user chose "Open in Safari" for a domain. Long-press the button and choose "Open in your app" to undo it.
In-app browsers (Instagram, TikTok, Facebook, LinkedIn, Snapchat) often refuse to hand links to apps. The banner still shows; the tap lands on DL2's page for that browser, which offers the store and explains how to open the system browser.
Simulators: the banner logic reads the user agent, so desktop browser device emulation shows it. Opening the app needs a real device or a simulator with the app installed; the iOS Simulator has no App Store, so the "not installed" path ends on the store page error.
The banner does not appear: it was dismissed in the last dismissDays days (remove dl2_banner_dismissed from localStorage), init was not called first, or the page is on a desktop browser without qr: true. Called from a script in <head>, the banner is added once the page has loaded (DOMContentLoaded).
Content Security Policy: the SDK adds one <style> element and calls your API host. Pass your page's nonce as banner({ nonce }) when the policy has style-src 'nonce-...'; otherwise allow style-src 'unsafe-inline' (or copy the styles into your own stylesheet and build a custom banner with the helpers), connect-src https://api.dl2.app, and img-src https://myapp.dl2.app for the QR card.
Privacy
- No cookies, no fingerprinting, no advertising identifiers.
localStorageholds one timestamp, written only when a visitor dismisses the banner.resolveandtracksend the link URL or the event you pass, plus a per-page random ID fortrack. The server sees the visitor's IP address and stores it only truncated or hashed.- Nothing is sent until you call
resolveortrack. The banner makes no network request, except loading the icon you chose and, on desktop withqr: true, the QR image from your link host.
If your site has a consent banner, track is the only call that records visitor activity. Call it according to your own consent rules.
Development
npm run typecheckTests live in the repository root: npx vitest run tests/sdk-web.test.ts.
