v 0.1.104

Client-Side Navigation

The @ecopages/browser-router package enables Single Page Application (SPA) navigation behavior in your Ecopages application. It intercepts link clicks, fetches the next page via fetch, and uses morphdom to efficiently diff and update only the parts of the DOM that changed.

This preserves element state (like audio players, web component internals, or form values) and enables native View Transitions.

Installation

Please run the following command to install the package:

bunx jsr add @ecopages/browser-router

Setup

To enable client-side routing, initialize the router in your global script (e.g., src/layouts/base-layout.script.ts).

The simplest approach is to use createRouter, which creates and starts the router in one call:

import { createRouter } from '@ecopages/browser-router/client';
 
const router = createRouter({
  viewTransitions: true,
});

For manual control over when the router starts and stops, use the EcoRouter class directly:

import { EcoRouter } from '@ecopages/browser-router/client';
 
const router = new EcoRouter({
  viewTransitions: true,
});
 
router.start();

Configuration

You can customize the router behavior by passing an options object:

OptionTypeDefaultDescription
linkSelectorstring'a[href]'Selector for links to intercept.
persistAttributestring'data-eco-persist'Attribute to mark elements that should persist across navigations.
reloadAttributestring'data-eco-reload'Attribute (on link or element) to force a full hard reload.
scrollBehavior'top' | 'preserve' | 'auto''top'Controls scroll behavior after navigation.
smoothScrollbooleanfalseEnables smooth scrolling.
viewTransitionsbooleantrueEnables View Transitions API support.
prefetchPrefetchConfig | falseObjectConfiguration for prefetching behavior.

View Transitions

The browser router supports the native View Transitions API.

To create a shared element transition, simply add the data-view-transition attribute to matching elements on different pages:

<!-- Page 1 -->
<div data-view-transition="hero-1">...</div>
 
<!-- Page 2 -->
<div data-view-transition="hero-1">...</div>

The router automatically handles the transition names and ensures a clean "morph" animation (hiding the old snapshot to prevent ghosting) by default.

Directives

You can control the transition behavior using data-view-transition-animate:

ValueDescription
morph(Default) Disables the cross-fade animation, resulting in a clean geometric morph. Ideal for shared element transitions to prevent "ghosting".
fadeUses the standard browser cross-fade animation. Useful if you specifically want the old and new elements to fade into each other.
<!-- Example: Opt-out of the default morph -->
<div data-view-transition="hero-1" data-view-transition-animate="fade">...</div>

Duration

You can also control the speed of a specific transition using data-view-transition-duration:

<div data-view-transition="hero-1" data-view-transition-duration="500ms">...</div>

Update History

| updateHistory | boolean | true | Whether to push a new entry to the browser history for each client-side navigation. | | smoothScroll | boolean | true | Whether to use smooth scrolling when adjusting scroll position after navigation. |

Example with Options

const router = createRouter({
  viewTransitions: true,
  scrollBehavior: 'preserve',
  linkSelector: 'a:not([data-no-route])',
});

Features

Persistence

Elements marked with data-eco-persist are never recreated during navigation. morphdom recognizes these elements by their persist ID and skips updating them entirely, preserving their internal state (event listeners, web component state, form values, audio playback, etc.).

Add the data-eco-persist attribute with a unique ID:

<audio 
  controls 
  src="/music.mp3" 
  data-eco-persist="global-player" 
/>

Script Re-execution

By default, scripts in the are not re-executed during navigation if they already exist. However, some scripts (like hydration logic or analytics) need to run on every page load.

To force a script to re-execute on navigation:

  1. Add data-eco-rerun="true" to force execution.
  2. Add data-eco-script-id="unique-id" to identify the script (preventing duplicate downloads).
<script 
  type="module" 
  data-eco-rerun="true" 
  data-eco-script-id="analytics-tracker"
>
  // This runs on initial load AND every navigation
  initAnalytics(window.location.pathname);
</script>

How it works:

Prefetching

The router includes a smart prefetching system to speed up navigation. By default, it uses the intent strategy, which prefetches pages when the user hovers over a link or when a link enters the viewport.

Configuration

You can configure prefetching in the createRouter options:

const router = createRouter({
  prefetch: {
    // 'intent' (default) | 'hover' | 'viewport'
    strategy: 'intent',
    // Delay in ms before prefetching on hover (default: 65)
    delay: 65,
    // Attribute to disable prefetching on specific links
    noPrefetchAttribute: 'data-eco-no-prefetch',
    // Whether to respect data-saver mode (default: true)
    respectDataSaver: true
  }
});

To disable prefetching entirely, set prefetch: false.

Strategies

StrategyDescription
intent(Recommended) Prefetches on hover (with delay) and acts as a fallback for viewport. Balances speed and bandwidth.
hoverPrefetches only when the user hovers over a link.
viewportPrefetches links as soon as they enter the viewport using IntersectionObserver.

Per-Link Control

You can override prefetching behavior on individual links:

<!-- Disable prefetching for this link -->
<a href="/heavy-page" data-eco-no-prefetch>Heavy Page</a>
 
<!-- Force eager prefetching (immediately on load) -->
<a href="/next-page" data-eco-prefetch="eager">Next Page</a>
 
<!-- Use a specific strategy for this link -->
<a href="/about" data-eco-prefetch="hover" data-eco-prefetch-delay="200">About</a>

View Transitions

If viewTransitions: true is enabled, Ecopages will trigger a View Transition on navigation. You can customize the animation using standard CSS or the provided optional styles.

Using Included Styles

The ecopages npm package includes default view transition animations. Import them in your CSS:

@import 'ecopages/css/view-transitions.css';

Then use the data-eco-transition attribute on elements to apply specific animations:

ValueDescription
fadeFades the element in and out during transition.
slideSlides the element horizontally during transition.
zoomScales the element in and out during transition.
slide-upSlides the element vertically upward during transition.
slide-downSlides the element vertically downward during transition.
<main data-eco-transition="slide">
  <!-- Content slides in -->
</main>

Lifecycle Events

The router emits lifecycle custom events on the document object, allowing you to hook into the navigation process.

EventDetailDescription
eco:before-swap{ url, direction, newDocument, reload }Fired after fetching new content but before updating the DOM. Use newDocument to inspect upcoming content. Call reload() to force a hard reload.
eco:after-swap{ url, direction }Fired after the DOM has been updated. Useful for re-initializing scripts or analytics.
eco:page-load{ url, direction }Fired on both initial load and subsequent navigations.

Example: Re-initializing Scripts

document.addEventListener('eco:after-swap', () => {
  console.log('Page updated!');
  // Re-run any page-specific logic here
});

Example: Navigation-Aware Components

For components that need to react to URL changes (like updating active states), listen for eco:page-load:

document.addEventListener('eco:page-load', () => {
  // Update active states, re-highlight nav links, etc.
  highlightActiveLink();
});