Route Dispatcher

The Route Dispatcher is the core routing system in SSE that maps URL paths to Page classes and manages the execution lifecycle.

Overview

The RouteDispatcher handles:

  • Matching URL paths to Page classes

  • Managing Site-level code execution

  • Coordinating the setup/execution lifecycle

  • Supporting wildcard routes for CMS collections

Basic Usage (v2.0+)

Creating the Dispatcher

In src/routes.ts, create and configure your RouteDispatcher:

import { RouteDispatcher, getAllPages } from "@sygnal/sse-core";
import { Site } from "./site";

// Import pages to trigger @page decorator registration
import "./pages/home";
import "./pages/about";
import "./pages/blog";

export const routeDispatcher = (): RouteDispatcher => {
    const dispatcher = new RouteDispatcher(Site);
    dispatcher.routes = getAllPages();  // Auto-populated from @page decorators
    return dispatcher;
}

Using the Dispatcher

In src/index.ts, create the dispatcher once and reuse it:

import { routeDispatcher } from './routes';

// Create dispatcher once
const dispatcher = routeDispatcher();

// Setup phase (runs in <head>)
dispatcher.setupRoute();

// Execution phase (runs after DOM ready)
window.Webflow ||= [];
window.Webflow.push(() => {
  dispatcher.execRoute();
});

Route Mapping

New in v2.0: Use the @page decorator for automatic route registration:

import { PageBase, page } from '@sygnal/sse-core';

@page('/')
export class HomePage extends PageBase {
  protected async onLoad(): Promise<void> {
    console.log('This is the homepage.');
  }
}

@page('/about')
export class AboutPage extends PageBase {
  protected async onLoad(): Promise<void> {
    console.log('This is the about page.');
  }
}

@page('/blog/*')
export class BlogPage extends PageBase {
  protected async onLoad(): Promise<void> {
    console.log('This is a blog post.');
    console.log('Item slug:', this.pageInfo.itemSlug);
  }
}

Then import these pages in routes.ts to register them automatically.

Manual Route Registration (Legacy)

You can still manually define routes if needed:

import { RouteDispatcher } from "@sygnal/sse-core";
import { Site } from "./site";
import { HomePage } from "./pages/home";
import { AboutPage } from "./pages/about";
import { BlogPage } from "./pages/blog";

export const routeDispatcher = (): RouteDispatcher => {
    const dispatcher = new RouteDispatcher(Site);
    dispatcher.routes = {
        '/': HomePage,
        '/about': AboutPage,
        '/blog/*': BlogPage,
    };
    return dispatcher;
}

Wildcard Routes

Wildcard paths use a trailing /* to match dynamic segments:

@page('/blog/*')
export class BlogPage extends PageBase {
  protected async onLoad(): Promise<void> {
    // Matches: /blog/post-1, /blog/post-2, /blog/any-slug
    console.log('Current slug:', this.pageInfo.itemSlug);
  }
}

@page('/products/*')
export class ProductPage extends PageBase {
  protected async onLoad(): Promise<void> {
    // Matches: /products/item-a, /products/item-b
    console.log('Product slug:', this.pageInfo.itemSlug);
  }
}

Wildcard Behavior:

  • /blog/* matches /blog/post-1 but NOT /blog itself

  • To match both, use two decorators:

    @page('/blog')
    @page('/blog/*')
    export class BlogPage extends PageBase { }

Execution Lifecycle

The RouteDispatcher manages a two-phase lifecycle:

Phase 1: Setup (Synchronous)

Runs during <head> load via dispatcher.setupRoute():

  1. Site's setup() method executes

  2. Matched Page's onPrepare() method executes

  3. Matched Components' onPrepare() methods execute

// This runs in <head> before DOM is fully loaded
dispatcher.setupRoute();

Phase 2: Execution (Asynchronous)

Runs after DOM ready via dispatcher.execRoute():

  1. Site's exec() method executes

  2. Matched Page's onLoad() method executes

  3. Matched Components' onLoad() methods execute

// This runs after DOM is ready
window.Webflow ||= [];
window.Webflow.push(() => {
  dispatcher.execRoute();
});

Instance Persistence (Critical Fix)

Wrong Approach (Data Loss)

// ❌ BROKEN - Creates two different instances
routeDispatcher().setupRoute();   // Instance A stores data
routeDispatcher().execRoute();    // Instance B has no data - LOST!

This creates two separate RouteDispatcher instances. Any data stored during setupRoute() is lost because execRoute() runs on a different instance.

Correct Approach (Data Preserved)

// ✅ CORRECT - Single instance preserves data
const dispatcher = routeDispatcher();
dispatcher.setupRoute();   // Instance stores data
dispatcher.execRoute();    // SAME instance has the data

This ensures the same RouteDispatcher instance is used for both phases, preserving all data.

Route Matching Logic

The dispatcher matches routes in the following order:

  1. Exact matches first: /about matches before /about/*

  2. Wildcard matches second: /blog/* matches /blog/post-1

  3. No match: No page code executes (Site code still runs)

Example:

// routes.ts
dispatcher.routes = {
    '/': HomePage,           // Exact: /
    '/blog': BlogIndexPage,  // Exact: /blog
    '/blog/*': BlogPostPage, // Wildcard: /blog/anything
};

// URL: / → HomePage
// URL: /blog → BlogIndexPage
// URL: /blog/my-post → BlogPostPage
// URL: /about → No page code (only Site)

Accessing Route Information

When using PageBase, route information is automatically available:

@page('/blog/*')
export class BlogPage extends PageBase {
  protected async onLoad(): Promise<void> {
    console.log('Path:', this.pageInfo.path);           // /blog/my-post
    console.log('Page ID:', this.pageInfo.pageId);      // Webflow page ID
    console.log('Collection:', this.pageInfo.collectionId); // CMS collection
    console.log('Item Slug:', this.pageInfo.itemSlug);  // my-post
  }
}

Multiple Routes Per Page

Use multiple @page decorators to handle multiple routes with one class:

@page('/about')
@page('/about-us')
@page('/team')
export class AboutPage extends PageBase {
  protected async onLoad(): Promise<void> {
    // Check which route was accessed
    if (this.pageInfo.path === '/team') {
      this.showTeamSection();
    }
  }
}

Best Practices

  1. Create dispatcher once - Store in a variable and reuse for both setup and exec

  2. Use @page decorator - Simplifies route registration

  3. Import all pages - Import page files in routes.ts to trigger decorators

  4. Wildcard for CMS - Use /* for collection template pages

  5. Extend PageBase - Get automatic context detection and pageInfo access

Example: Complete Setup

Here's a complete example showing proper dispatcher usage:

src/routes.ts:

import { RouteDispatcher, getAllPages } from "@sygnal/sse-core";
import { Site } from "./site";

// Import all pages
import "./pages/home";
import "./pages/about";
import "./pages/blog";
import "./pages/products";

// Import all components
import "./components/navigation";
import "./components/footer";

export const routeDispatcher = (): RouteDispatcher => {
    const dispatcher = new RouteDispatcher(Site);
    dispatcher.routes = getAllPages();
    return dispatcher;
}

src/index.ts:

import { routeDispatcher } from './routes';

// Create once
const dispatcher = routeDispatcher();

// Setup phase
dispatcher.setupRoute();

// Execution phase
window.Webflow ||= [];
window.Webflow.push(() => {
  dispatcher.execRoute();
});

src/pages/blog.ts:

import { PageBase, page } from '@sygnal/sse-core';

@page('/blog/*')
export class BlogPage extends PageBase {

  protected onPrepare(): void {
    console.log('Blog page preparing...');
  }

  protected async onLoad(): Promise<void> {
    console.log('Blog post loaded:', this.pageInfo.itemSlug);

    // Your blog-specific code here
    this.loadComments();
    this.setupSocialSharing();
  }

  private async loadComments() {
    // Load comments for this post
  }

  private setupSocialSharing() {
    // Setup social sharing buttons
  }
}

Last updated