npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 localStorage key that remembers a dismissed banner.
  • Nothing throws: every function is safe to call on any page, including during server rendering.

Install

npm install @dl2app/web

Set 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: true and link are set. Then it shows a small card with the QR image https://myapp.dl2.app/_/qr/welcome.svg to 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.app on iOS and a verified intent filter for it on Android. The Expo plugin of @dl2app/react-native adds 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.json
  • If 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.
  • localStorage holds one timestamp, written only when a visitor dismisses the banner.
  • resolve and track send the link URL or the event you pass, plus a per-page random ID for track. The server sees the visitor's IP address and stores it only truncated or hashed.
  • Nothing is sent until you call resolve or track. The banner makes no network request, except loading the icon you chose and, on desktop with qr: 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 typecheck

Tests live in the repository root: npx vitest run tests/sdk-web.test.ts.