A TypeScript-based template for building custom Webflow site extensions using the Sygnal Site Engine (SSE2) framework.
- TypeScript - Type-safe development with modern ES6+ features
- SCSS Support - Write maintainable styles with variables, nesting, and mixins
- Fast Build System - TypeScript type checking + esbuild bundler, Dart Sass for SCSS
- Watch Mode - Auto-rebuild on file changes during development
- Component System - Reusable, attribute-based components with automatic context detection
- Route Management - Page-based architecture with automatic Webflow context
- Source Maps - Debug TypeScript and SCSS in browser DevTools
- Development Server - Local testing with hot reload
npm installUpdate package.json:
- Set
nameto your project name - Set
versionappropriately
Update src/index.ts:
- Edit
SITE_NAMEconstant - Edit
VERSIONconstant
npm run build # Development build with type checking
npm run build:prod # Production build with minificationThis performs type checking, bundles all TypeScript into a single dist/index.js, and compiles SCSS files to dist/.
npm run watch # Start watch mode for auto-rebuild
npm run serve # Start local dev server (http://127.0.0.1:3000)Run both commands in separate terminals for the best development experience.
If you already have a local project with its own git repo and want to pull this template's files into it — without inheriting the template's commit history — use git to drop the template contents into your working tree:
git init # skip if the repo already exists
git remote add template https://github.com/sygnaltech/sse-template.git
git fetch template --depth=1
git checkout template/main -- . # drop template files into the working tree, NO template history
git remote remove template # detach from the templateThen install and build:
npm install
npm run buildWhat this does: git checkout template/main -- . copies the template's files into your
working tree and stages them. Your repo keeps its own history; none of the template's commits
are imported. Removing the remote detaches you from the template entirely.
⚠️ This overwrites same-named files. Any file that exists in both your project and the template (e.g.package.json,tsconfig.json,README.md,.gitignore,build.js) is replaced by the template's version. Commit or stash your work first so you can review and revert. After checkout, rungit statusandgit diff --cachedto see exactly what landed.For
package.jsonandtsconfig.jsonyou'll usually want to merge rather than accept the template wholesale — keep your existing fields and add the template's dependencies, scripts, and the required compiler options (experimentalDecorators,emitDecoratorMetadata,resolveJsonModule,"DOM"inlib).
Pulling template updates later: re-add the remote, fetch, and check out just the paths you
want — e.g. git checkout template/main -- build.js — then remove the remote again.
sse-template/
├── src/
│ ├── index.ts # Main entry point (only file bundled)
│ ├── site.ts # Site-level module
│ ├── registry.ts # ⭐ Central registry: import all pages/components/actions here
│ ├── types.ts # Template-specific types
│ ├── version.ts # Version (re-exported from package.json)
│ ├── site.scss # Global styles
│ ├── pages/
│ │ ├── home.ts # Home page module
│ │ ├── blog.ts # Blog page (wildcard example)
│ │ └── about.ts # About page (multi-route example)
│ ├── components/
│ │ ├── test.ts # Test component
│ │ └── example.ts # Example component
│ └── actions/
│ └── example-action.ts # Example FIX action
├── dist/ # Compiled output (git-ignored)
├── build.js # Build script (tsc + esbuild + sass)
├── package.json # Dependencies & scripts
├── tsconfig.json # TypeScript config
├── docs/ # Migration guides (SSE2, FIX)
├── AGENTS.md # Authoritative guide for AI agents
└── README.md # This file
Note:
registry.tsis the single place where pages, components, and actions are registered — importing a module here is what triggers its@page/@component/@actiondecorator. A module that isn't imported inregistry.tsnever runs.
| Command | Description |
|---|---|
npm run build |
Development build with type checking + bundling |
npm run build:prod |
Production build with minification (for deployment) |
npm run watch |
Watch mode - auto-rebuild TypeScript and SCSS |
npm run typecheck |
Run TypeScript type checking only |
npm run serve |
Start local dev server on port 3000 |
npm run format |
Format TypeScript and SCSS with Prettier |
npm run clean |
Remove dist directory |
- Create
.scssfiles anywhere in thesrc/directory - The build process automatically compiles them to
dist/with the same folder structure - Output is compressed (minified) for production
Example:
src/site.scss → dist/site.css
src/pages/home.scss → dist/pages/home.css
@use "sass:color";
@use "sass:math";
// Variables
$primary: #007bff;
$spacing: 1rem;
// Nesting
.component {
padding: $spacing;
&:hover {
background: color.scale($primary, $lightness: 80%);
}
}
// Mixins
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}The SSE framework can auto-load CSS files. See src/site.ts for an example:
Page.loadEngineCSS("site.css");Pages are automatically discovered using the @page decorator. Just create and import!
// src/pages/about.ts
import { PageBase, page } from '@sygnal/sse-core';
@page('/about') // ← Decorator auto-registers this route!
export class AboutPage extends PageBase {
protected onPrepare(): void {
// Synchronous setup - called during <head> load
// Access Webflow page context automatically
console.log('Page ID:', this.pageInfo.pageId);
console.log('Collection:', this.pageInfo.collectionId);
}
protected async onLoad(): Promise<void> {
// Asynchronous execution - called after DOM ready
console.log('About page loaded');
console.log('Current path:', this.pageInfo.path);
}
}// ============================================================
// PAGES - Import all pages to trigger @page decorator
// ============================================================
import "./pages/home";
import "./pages/about"; // ← Add your import here!The route is automatically registered. No manual route mapping needed!
Note: All pages, components, and actions are imported in registry.ts - this is the central registry location.
When you extend PageBase, your page automatically gets access to this.pageInfo:
protected async onLoad(): Promise<void> {
// All available automatically:
console.log(this.pageInfo.path); // Current path
console.log(this.pageInfo.url); // Full URL
console.log(this.pageInfo.pageId); // Webflow page ID
console.log(this.pageInfo.siteId); // Webflow site ID
console.log(this.pageInfo.collectionId); // CMS collection ID (if applicable)
console.log(this.pageInfo.itemId); // CMS item ID (if applicable)
console.log(this.pageInfo.itemSlug); // CMS item slug (if applicable)
console.log(this.pageInfo.queryParams); // URLSearchParams object
console.log(this.pageInfo.hash); // URL hash
console.log(this.pageInfo.domain); // Webflow domain
console.log(this.pageInfo.lang); // Page language
}You can use multiple @page decorators on a single class to handle multiple routes:
// src/pages/about.ts
import { PageBase, page } from '@sygnal/sse-core';
@page('/about') // All three routes
@page('/about-us') // use the same
@page('/team') // page class!
export class AboutPage extends PageBase {
protected onPrepare(): void {
console.log('Preparing about page...');
}
protected async onLoad(): Promise<void> {
// Check which route was accessed via pageInfo
const currentPath = this.pageInfo.path;
if (currentPath === '/team') {
// Show team-specific content
}
}
}Use cases:
- Route aliases (
/shop,/store,/products) - Localized URLs (
/en/contact,/es/contacto) - Legacy URL support
- Similar pages with shared logic
Wildcard routes are supported using * for dynamic paths:
// src/pages/blog.ts
import { PageBase, page } from '@sygnal/sse-core';
@page('/blog/*') // ← Matches /blog/post-1, /blog/category/tech, etc.
export class BlogPage extends PageBase {
protected onPrepare(): void {
console.log('Blog page preparing...');
}
protected async onLoad(): Promise<void> {
// Access the full path via pageInfo
const fullPath = this.pageInfo.path;
const slug = fullPath.replace('/blog/', '');
console.log('Blog slug:', slug);
// If this is a Webflow CMS collection page
if (this.pageInfo.itemSlug) {
console.log('CMS Item Slug:', this.pageInfo.itemSlug);
console.log('Collection ID:', this.pageInfo.collectionId);
}
}
}Routes are matched in the order they're registered, with exact matches taking precedence over wildcards.
Components are automatically discovered using the @component decorator and extend ComponentBase for automatic element context.
// src/components/my-component.ts
import { ComponentBase, component, PageBase } from '@sygnal/sse-core';
@component('my-component') // ← Decorator auto-registers this component!
export class MyComponent extends ComponentBase {
protected onPrepare(): void {
// Synchronous setup - runs before DOM ready
// Access element and context automatically
console.log('Component name:', this.context.name);
console.log('Component ID:', this.context.id);
console.log('Data attributes:', this.context.dataAttributes);
}
protected async onLoad(): Promise<void> {
// Asynchronous execution - runs after DOMContentLoaded
console.log('MyComponent initialized on:', this.element);
// Access current page info via singleton
const page = PageBase.getCurrentPage();
if (page) {
console.log('Component on page:', page.pageInfo.pageId);
console.log('Collection item:', page.pageInfo.itemSlug);
}
// Add event listeners
this.element.addEventListener('click', () => {
console.log('Component clicked!');
});
}
}// ============================================================
// COMPONENTS - Import all components to trigger @component decorator
// ============================================================
import "./components/test";
import "./components/my-component"; // ← Add your import here!<div sse-component="my-component" sse-component-id="nav-main">
<!-- Component content -->
</div>The component is automatically discovered and instantiated!
When you extend ComponentBase, your component automatically gets:
this.element- The HTMLElement the component is bound tothis.context.name- Component name fromsse-componentattributethis.context.id- Component ID fromsse-component-idattribute (if present)this.context.dataAttributes- All data-* attributes on the element
Components can access the current page via the singleton pattern:
protected async onLoad(): Promise<void> {
const page = PageBase.getCurrentPage();
if (page) {
// Access all page info
console.log('Page ID:', page.pageInfo.pageId);
console.log('Collection:', page.pageInfo.collectionId);
console.log('Item Slug:', page.pageInfo.itemSlug);
// Component logic based on page context
if (page.pageInfo.collectionId === 'products') {
// Product-specific component behavior
}
}
}- Auto-discovery: Components are automatically found and initialized
- Type-safe: Full TypeScript support with strict typing
- Component Manager: All instances registered in
window.componentManager - Error handling: Graceful error handling with console warnings
- Lifecycle hooks:
onPrepare()andonLoad()methods for different initialization phases - Automatic context: Element and data attributes automatically detected
// Get all instances of a specific component type
const myComponents = window.componentManager.getComponentsByType<MyComponent>('my-component');
// Get all registered component types
const types = window.componentManager.getComponentTypes();
// Get total count
const total = window.componentManager.getTotalCount();FIX is a declarative, event-driven interaction system: HTML attributes fire named events, and
TypeScript actions respond to them. It's re-exported from @sygnal/sse-core (implemented in
@sygnal/fix) and initialized automatically via initializeFIX() in index.ts.
// src/actions/example-action.ts
import { ActionBase, action, type TriggerData } from "@sygnal/sse-core";
@action('example-action')
export class ActionExample extends ActionBase {
init(): void {
// Runs once when the action is registered
}
async trigger(triggerElement: HTMLElement, triggerData: TriggerData): Promise<void> {
// Runs when the bound event fires.
// triggerData carries values from trigger:click:data:* attributes
const message = triggerData['message'] || 'Hello!';
console.log(message);
}
}import { ActionExample } from "./actions/example-action";
registerProgrammaticAction('example-action', 'example-event', ActionExample);registerProgrammaticAction(actionType, eventName, ActionClass) connects the action to a named event.
<button trigger:click="example-event" trigger:click:data:message="Custom message">
Click Me
</button>trigger:click is built in. Extra trigger:click:data:* attributes are passed to the action as
triggerData.
From the browser console:
FIXDebug.stats() // Full system stats
FIXDebug.triggerTypes() // Registered trigger types
FIXDebug.actionTypes() // Registered action types
FIXDebug.triggers() // Active trigger instances on the page
FIXDebug.actions() // Active action instances on the page
FIXDebug.events() // Registered eventsSee docs/02-MIGRATION-FIX.md for the full FIX reference.
Add to your Webflow site's Custom Code section (before </head> or before </body>):
Replace REPO with your GitHub repository name and update the version number:
<!-- Site Engine -->
<script
src="https://cdn.jsdelivr.net/gh/sygnaltech/REPO@0.1.0/dist/index.js"
dev-src="http://127.0.0.1:3000/dist/index.js"
></script><!-- Site Engine -->
<script src="http://127.0.0.1:3000/dist/index.js"></script>The SSE framework supports query parameters for switching modes:
?engine.mode=dev- Force development mode?engine.mode=prod- Force production mode
The template uses a hybrid build approach combining TypeScript's type checking with esbuild's fast bundling.
- Type Check - TypeScript compiler validates all types (
tsc --noEmit) - Bundle - esbuild bundles
src/index.tsand all imports into singledist/index.js - SCSS Compile - Dart Sass compiles all
.scssfiles to.css
Development Build (npm run build):
- Full type checking
- Unminified output for easier debugging
- Source maps included
- ~15KB bundle size
Production Build (npm run build:prod):
- Full type checking
- Minified output
- Source maps included
- ~6.8KB bundle size (smaller, faster loading)
npm run watch- TypeScript auto-rebuilds on file changes (no type check in watch for speed)
- SCSS auto-compiles on file changes
- Runs continuously until stopped
- tsc provides comprehensive type checking
- esbuild provides extremely fast bundling
- Single bundle (
dist/index.js) loads all dependencies - No duplicate code (all imports bundled once)
- Best of both worlds: safety + speed
The SSE framework uses a two-phase initialization:
-
onPrepare()- Runs synchronously during<head>load, before DOM is ready- Good for: Configuration, early initialization, reading data attributes
-
onLoad()- Runs asynchronously afterDOMContentLoaded- Good for: DOM manipulation, event binding, API calls
All pages extend PageBase and components extend ComponentBase:
// Pages get automatic Webflow context
export abstract class PageBase {
protected pageInfo: WebflowPageInfo; // Auto-populated
protected abstract onPrepare(): void;
protected abstract onLoad(): Promise<void>;
}
// Components get automatic element context
export abstract class ComponentBase {
protected element: HTMLElement; // Auto-provided
protected context: ComponentContext; // Auto-populated
protected abstract onPrepare(): void;
protected abstract onLoad(): Promise<void>;
}Components are automatically discovered via the sse-component attribute and instantiated by the framework.
- @sygnal/sse-core - SSE framework core with PageBase and ComponentBase
- gsap - Animation library (optional)
- js-cookie - Cookie handling utility
- esbuild - Fast JavaScript bundler
- sass - Dart Sass compiler
- typescript - TypeScript compiler
- prettier - Code formatter
- serve - Development server
Test Project: https://webflow.com/dashboard/sites/sygnal-site-engine/general
See package.json for license information.