# Web (Kotlin/JS and Kotlin/Wasm) Both `rinku-core` and `rinku-compose-ext` support `js { browser() }` and `wasmJs { browser() }`. Use the same Maven coordinates and shared `DeepLink`, filters, mappers, and listeners as on Android, iOS, and desktop. Rinku is a deep-link receiver, not a Web router. ## Browser contract `RinkuWeb` receives: - The full current `window.location.href` at startup, including the site root, unless `captureInitialUrl = false`. - The current full URL on each `popstate` event, such as browser Back and Forward. Rinku does **not** write browser history, intercept `pushState` or `replaceState`, observe `hashchange`, or convert a fragment into a route. Path routing is supported; hash routing is outside this adapter's contract. A fragment in a received URL is preserved in `DeepLink.data`, not interpreted as path segments. Filters run before mappers, once per browser event. The mapper's output goes through the existing common parser and bounded FIFO queue. Up to 64 platform links can wait for a listener; overflow rejects the newest link and calls `onError`. Distinct browser events with identical URLs are not deduplicated. ## Manual initialization In your Web entry point, start the adapter before creating the UI: ```kotlin import dev.theolm.rinku.RinkuWeb val rinku = RinkuWeb( captureInitialUrl = true, deepLinkFilter = myFilter, // optional deepLinkMapper = myMapper, // optional onError = { error -> println("Deep-link error: $error") }, ) rinku.start() // Create your UI. Call rinku.close() when disposing the application. ``` Use only one active adapter per page. Repeated `start()` calls do nothing; they do not replay the initial URL. `close()` is idempotent, removes the browser listener, and leaves already queued links available to shared listeners. A closed adapter cannot restart; create a new one instead. Starting another adapter while one is active throws. Filter/mapper, parsing, and overflow failures go to `onError`. Initialization/ownership errors throw to the caller. Error callbacks should not throw. The default callback prints the exception. Filters and mappers supplied at startup are also registered for explicit `Rinku.handleDeepLink(..., applyFilter = true, applyMapper = true)` calls, as on desktop. ## Compose initialization Instead of initializing manually, wrap the root of your Web composition: ```kotlin import dev.theolm.rinku.compose.ext.Rinku Rinku( captureInitialUrl = true, deepLinkFilter = myFilter, deepLinkMapper = myMapper, ) { App() } ``` The wrapper uses `DisposableEffect` to start/close its adapter. Ordinary recomposition does not reinstall it or replay startup. Changing capture/filter/mapper configuration replaces the adapter, capturing the initial URL again if enabled. Remember filter/mapper objects rather than constructing new instances on every recomposition. Updating `onError` alone does not restart the adapter. Do not combine the wrapper with a manually started adapter. ## Shared navigation remains unchanged Use one logical listener, either the suspending common API: ```kotlin listenForDeepLinks { deepLink -> // Application-specific parsing/navigation, just as on the other platforms. } ``` or the existing Compose listener inside `App()`: ```kotlin DeepLinkListener { deepLink -> // Update Voyager, Decompose, or your own navigation. } ``` Events are consumed once, not broadcast. Initial URLs received before the listener starts remain queued. The adapter never depends on a navigation library. ## Application-owned navigation and history `pushState` and `replaceState` do not emit `popstate`. When navigating internally, the application must update its router itself, or explicitly notify Rinku **once**: ```kotlin window.history.pushState(null, "", "/second/third?source=web") Rinku.handleDeepLink(window.location.href, applyFilter = true, applyMapper = true) ``` Enable the flags only if you configured the respective filter/mapper. Do not both navigate directly and dispatch the same link if that would duplicate the transition. Do not write a new history entry from a Back/Forward callback: doing so creates a navigation loop. Do not install a second browser-to-router integration for the same events without coordinating it. ## Hosting and deployment prefixes Rinku preserves the URL, including deployment prefixes. For `https://example.com/app/second?source=web`, the shared path contains `app` and `second`. If navigation should see only `second`, remove the prefix in your `DeepLinkMapper`, preserving the origin, encoded path, query, and fragment. There is no adapter-level `basePath` setting. The host must serve your SPA entry point for application routes such as `/second/third`, including direct requests and reloads. Static resources must still resolve from the deployment root. For example, configure an SPA rewrite/fallback on your host, and use `` when deploying under `/app/`. Rinku cannot fix server-side 404s. ## Samples Both navigation samples include a `webApp` module. Run one command at a time: ```shell ./gradlew :samples:voyager:webApp:jsBrowserDevelopmentRun ./gradlew :samples:voyager:webApp:wasmJsBrowserDevelopmentRun ./gradlew :samples:decompose:webApp:jsBrowserDevelopmentRun ./gradlew :samples:decompose:webApp:wasmJsBrowserDevelopmentRun ``` Each sample has application-owned history controls. Open `/second/third?source=web` directly, navigate with the toolbar, then try browser Back, Forward, and reload. The development servers include an SPA fallback. Existing in-screen back controls operate on the navigation library's stack; the toolbar's browser controls operate on browser history. For a prefixed deployment, change both `` and the `rinku-base-path` meta tag in the sample's `src/webMain/resources/index.html` to the same slash-terminated prefix, such as `/app/`. The sample mapper removes that prefix before shared navigation. Its filter accepts same-origin URLs. Build distributable bundles with `jsBrowserDistribution` or `wasmJsBrowserDistribution` on either `webApp` module. Wasm requires a browser with Wasm GC support; Kotlin/JS is a separate build, not an automatic runtime fallback. ## Tests Install Chrome/Chromium and set `CHROME_BIN` if it is not discoverable. Run: ```shell ./gradlew :rinku-core:jsBrowserTest :rinku-core:wasmJsBrowserTest \ :rinku-compose-ext:jsBrowserTest :rinku-compose-ext:wasmJsBrowserTest ``` The suites run shared tests plus real browser-history tests and Compose runtime lifecycle tests. CI also bundles both samples in JS and Wasm. Linking Compose Web builds can require more than the project's default 2 GB compiler heap; CI uses `--max-workers=1 -Pkotlin.daemon.jvmargs=-Xmx4g` to serialize compilation with a larger heap. Use those options locally when linking the samples or Compose tests if needed. Yarn lockfiles are tracked; after intentionally changing npm dependencies, regenerate them with `./gradlew kotlinUpgradeYarnLock kotlinWasmUpgradeYarnLock`.