# Sygnal Site Engine ( SSE2 )

Structure, reuse, improve and protect your Webflow site code.

**Sygnal Site Engine ( SSE )** is a framework for *client-side* script development in Webflow-hosted sites. It combines a range of best-practices and modern technologies to create an ideal development environment for Webflow.

{% hint style="success" %}
**SSE v2.0.0 Released!** We've released a major update with new base classes, automatic Webflow context detection, and improved lifecycle methods. See [What's New](/whats-new) for details.
{% endhint %}

We focus on using...

* Modern development IDEs such as VS Code or Cursor
* Modern languages, specifically TypeScript and SASS
* Structured development practices- multiple classes and files to organize your code
* Source control to protect your code and history
* Development, test, and production environment separation
* CI/CD pipeline support to deploy your code changes
* Monorepo- a consolidated codebase approach for your Webflow client-side development to ensure smooth integration into your Webflow site
* Switchable debugging- turn on console logging centrally, and only when you need it

{% hint style="success" %}
**Zero Cost.** A complete setup can be built 100% free, as all of these technologies are freely available or utilize free plans. All it requires is learning the tech, and the setup.
{% endhint %}

## Core Technologies Used

SSE's architecture is highly flexible, but this is our typical configuration;

* **TypeScript** for structured, modern code development with type-safety. Makes code far more manageable, more reusable, better bug-proofing, and offers better testing.
* **SASS** for modern structured CSS development which support robust commenting
* **GitHub** for full source control management ( SCM ), ensures your code is safe, and that you have a full history of changes.
* **Jest** for full unit-testing support
* **VSCode** as a full modern IDE for development
* **Netlify** for the production code-delivery CDN.
* **Sygnal DevProxy** for full DEV/TEST and PROD environments. Allows your developers, team, and clients to fully test new features before they are public.

## Key Benefits

This setup gives us a ton of benefits;

* Easy code management in your Webflow site. There is no code editing anywhere, on any pages through the Webflow designer. Everything is done directly in VS code.
* Monorepo for code safety- there is no risk of breaking your site or deleting important pieces, everything is safe in GitHub and you can revert any time.
* Solid versioning, tracking, and production deployment controls.
* Realtime development. Change, save, refresh, you're looking at live code against your current Webflow staged site.
* Persistent testing. Check in code anytime, and your client and testing team can review the latest changes immediately on e.g. `test.mysite.com` or whatever URL or subdomain you choose.
* Easy access to add-on libraries, like Luxon for dates, cookie.js for cookie management, or GSAP for animation.
* CI/CD setup for easy, live deployment support. There is no need to change code or URLs in Webflow, you can deploy your code updates to your production site through a simple GitHub pull request.

{% hint style="success" %}
We're continually adding new features as well, because our team utilizes this setup in all of our client sites.
{% endhint %}

## Video Tour

Here's a quick video overview of how SSE benefits our team and projects.

{% embed url="<https://www.loom.com/share/a65258f54d424511a1a1373428723922>" %}


# The SSE Architecture

How the SSE works with DevProxy and CI/CD setup

{% hint style="success" %}
**It's simpler than it looks.** \
Yes, we use quite a few different systems in our full stack, but none of them are complex and they each serve a specific purpose.&#x20;
{% endhint %}

<img src="https://3849716756-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FbZeVxnZi0ThYsR4w1hXd%2Fuploads%2F1Pmrhj3FScJMWfxNT19g%2Ffile.excalidraw.svg?alt=media&amp;token=3c2906e5-a68e-4d94-9b51-3a96c80be88f" alt="" class="gitbook-drawing">


# What's New

What's new with SSE?

## 10-Nov-2025 - v2.0.0 Major Release

SSE v2.0.0 introduces significant improvements with new base classes and automatic context detection.

### Package Renamed

* **@sygnal/sse** → **@sygnal/sse-core**
* Update your `package.json` and all import statements

### New Base Classes with Automatic Context

* **PageBase** - Automatic Webflow page context detection
  * Access `this.pageInfo` with pageId, siteId, collectionId, itemSlug, etc.
  * No manual DOM queries needed
* **ComponentBase** - Automatic element context detection
  * Access `this.element` and `this.context` automatically
  * Component metadata pre-populated

### Improved Lifecycle Methods

* **onPrepare()** - Synchronous setup (replaces `setup()`)
* **onLoad()** - Asynchronous execution (replaces `exec()`)
* More intuitive naming aligned with web lifecycle

### Singleton Pattern for Page Access

* Components can access current page via `PageBase.getCurrentPage()`
* Type-safe with generic support
* Eliminates duplicate context detection

### Critical Fix: RouteDispatcher Instance Persistence

* RouteDispatcher must now be created once and reused
* Prevents data loss between setup and execution phases
* See migration guide for updated pattern

### Backward Compatibility

* Old `IModule` interface still supported for advanced use
* Recommended: Migrate to new base classes for automatic features

See the [sse-template README](https://github.com/sygnaltech/sse-template) for detailed migration examples.

## 29-Jun-2024

* Deployed `@sygnal/sse` core functionality as an NPM package, so that core SSE functionality can be updated independently of the site-specific implementations.


# Migrating from SSE to SSE2

Key changes you'll need to make to utilize SSE2's new sse-core

## Overview

SSE v2.0.0 (sse-core) introduces significant improvements that make working with Webflow sites easier and more maintainable. This guide will walk you through migrating your existing SSE v1.x code to v2.0.0.

## Key Changes

1. **Package renamed**: `@sygnal/sse` → `@sygnal/sse-core`
2. **New base classes**: `PageBase` and `ComponentBase` with automatic context detection
3. **Lifecycle methods**: `setup()`/`exec()` → `onPrepare()`/`onLoad()`
4. **Singleton pattern**: Access page from components via `PageBase.getCurrentPage()`
5. **RouteDispatcher fix**: Must create once and reuse the instance

## Migration Steps

### Step 1: Update Package

Update your `package.json`:

```json
{
  "dependencies": {
    "@sygnal/sse-core": "^2.0.0"
  }
}
```

Then run:

```bash
npm install
```

### Step 2: Update Import Statements

Find and replace all imports throughout your project:

**Before:**

```typescript
import { IModule, page, component } from '@sygnal/sse';
```

**After:**

```typescript
import { PageBase, ComponentBase, page, component } from '@sygnal/sse-core';
```

### Step 3: Migrate Pages to PageBase

**Before (v1.x):**

```typescript
import { IModule, page } from '@sygnal/sse';

@page('/')
export class HomePage implements IModule {

  constructor() {}

  setup(): void {
    // Synchronous setup
    console.log('Setting up home page');
  }

  async exec(): Promise<void> {
    // Manual context detection
    const pageId = document.documentElement.getAttribute('data-wf-page');
    const collectionId = document.documentElement.getAttribute('data-wf-collection');

    console.log('Page ID:', pageId);
    console.log('Collection:', collectionId);
  }
}
```

**After (v2.0):**

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

@page('/')
export class HomePage extends PageBase {

  protected onPrepare(): void {
    // Synchronous setup - automatic context available!
    console.log('Setting up home page');
    console.log('Page ID:', this.pageInfo.pageId);
  }

  protected async onLoad(): Promise<void> {
    // Async execution - full context available
    console.log('Collection:', this.pageInfo.collectionId);
    console.log('Item Slug:', this.pageInfo.itemSlug);
    console.log('Query Params:', this.pageInfo.queryParams);
  }
}
```

**Key Changes:**

* `implements IModule` → `extends PageBase`
* Remove constructor (not needed)
* `setup()` → `protected onPrepare()`
* `exec()` → `protected async onLoad()`
* Access `this.pageInfo` for all Webflow context (no manual DOM queries!)

### Step 4: Migrate Components to ComponentBase

**Before (v1.x):**

```typescript
import { IModule, component } from '@sygnal/sse';

@component('navigation')
export class NavigationComponent implements IModule {
  private elem: HTMLElement;

  constructor(elem: HTMLElement) {
    this.elem = elem;
  }

  setup(): void {
    console.log('Navigation setup');
  }

  async exec(): Promise<void> {
    // Manual element access
    const componentName = this.elem.getAttribute('data-component');
    const customData = this.elem.getAttribute('data-nav-type');

    this.elem.addEventListener('click', () => {
      console.log('Navigation clicked');
    });
  }
}
```

**After (v2.0):**

```typescript
import { ComponentBase, component, PageBase } from '@sygnal/sse-core';

@component('navigation')
export class NavigationComponent extends ComponentBase {

  protected onPrepare(): void {
    // Automatic element and context available!
    console.log('Navigation setup');
    console.log('Component:', this.context.name);
  }

  protected async onLoad(): Promise<void> {
    // Access element directly
    const customData = this.element.getAttribute('data-nav-type');

    // Access current page if needed
    const page = PageBase.getCurrentPage();
    if (page) {
      console.log('Current page:', page.pageInfo.pageId);
    }

    this.element.addEventListener('click', () => {
      console.log('Navigation clicked');
    });
  }
}
```

**Key Changes:**

* `implements IModule` → `extends ComponentBase`
* Remove constructor (automatic element injection!)
* `setup()` → `protected onPrepare()`
* `exec()` → `protected async onLoad()`
* `this.elem` → `this.element` (automatic)
* Access `this.context` for component metadata
* Access page via `PageBase.getCurrentPage()` singleton

### Step 5: Fix RouteDispatcher Instance (CRITICAL)

This is a critical fix that prevents data loss between preparation and execution phases.

**Before (v1.x - BROKEN):**

```typescript
import { routeDispatcher } from './routes';

// This creates TWO different instances!
routeDispatcher().setupRoute();  // Instance A stores data
routeDispatcher().execRoute();   // Instance B has no data - LOST!
```

**After (v2.0 - CORRECT):**

```typescript
import { routeDispatcher } from './routes';

// Create ONCE and reuse the same instance
const dispatcher = routeDispatcher();
dispatcher.setupRoute();   // Instance stores data
dispatcher.execRoute();    // SAME instance has the data!
```

Update your `src/index.ts`:

```typescript
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();
});
```

### Step 6: Update Route Registration (Optional)

If using automatic route discovery with `getAllPages()`:

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

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

export const routeDispatcher = (): RouteDispatcher => {
    const dispatcher = new RouteDispatcher(Site);
    dispatcher.routes = getAllPages();  // Automatically populated!
    return dispatcher;
}
```

### Step 7: Keep Site Class Using IModule

The Site class doesn't need automatic context, so it still uses `IModule`:

```typescript
import { IModule } from "@sygnal/sse-core";

export class Site implements IModule {

  constructor() {}

  setup() {
    // Site-wide setup
  }

  exec() {
    // Site-wide execution
  }
}
```

## Available Automatic Context

### PageBase - this.pageInfo Properties

When extending `PageBase`, you automatically get access to:

* `this.pageInfo.path` - Current page path (e.g., "/blog/my-post")
* `this.pageInfo.url` - Full URL
* `this.pageInfo.hash` - URL hash fragment
* `this.pageInfo.queryParams` - Parsed query parameters object
* `this.pageInfo.pageId` - Webflow page ID
* `this.pageInfo.siteId` - Webflow site ID
* `this.pageInfo.collectionId` - CMS collection ID (if applicable)
* `this.pageInfo.itemId` - CMS item ID (if applicable)
* `this.pageInfo.itemSlug` - CMS item slug (if applicable)
* `this.pageInfo.domain` - Webflow domain
* `this.pageInfo.lang` - Page language

### ComponentBase - this Properties

When extending `ComponentBase`, you automatically get:

* `this.element` - The HTMLElement the component is bound to
* `this.context.name` - Component name from `data-component`
* `this.context.id` - Component ID from `data-component-id`
* `this.context.dataAttributes` - All data-\* attributes as key-value pairs

## Benefits Summary

| Feature                        | v1.x (IModule)            | v2.0 (Base Classes)         |
| ------------------------------ | ------------------------- | --------------------------- |
| **Context Detection**          | Manual DOM queries        | Automatic                   |
| **Page Info**                  | Parse attributes yourself | `this.pageInfo.*`           |
| **Component Element**          | Pass in constructor       | `this.element`              |
| **Lifecycle Naming**           | `setup()`/`exec()`        | `onPrepare()`/`onLoad()`    |
| **Page Access from Component** | Pass references manually  | `PageBase.getCurrentPage()` |
| **Type Safety**                | Manual typing             | Fully typed                 |
| **Boilerplate Code**           | Lots                      | Minimal                     |

## Testing Your Migration

After migrating, verify:

1. ✅ All pages load without console errors
2. ✅ Page-specific code executes on correct routes
3. ✅ Components initialize and function correctly
4. ✅ `this.pageInfo` contains expected values in pages
5. ✅ `this.element` and `this.context` work in components
6. ✅ `PageBase.getCurrentPage()` returns page instance from components
7. ✅ Data persists between `onPrepare()` and `onLoad()` phases

## Common Issues

### Issue: "Property 'pageInfo' does not exist"

**Cause:** Still using `implements IModule` instead of `extends PageBase`

**Fix:** Change class declaration:

```typescript
// Before
export class HomePage implements IModule {

// After
export class HomePage extends PageBase {
```

### Issue: Data lost between onPrepare() and onLoad()

**Cause:** Creating multiple RouteDispatcher instances

**Fix:** Store dispatcher in a variable and reuse:

```typescript
const dispatcher = routeDispatcher();
dispatcher.setupRoute();
dispatcher.execRoute();
```

### Issue: "Cannot read property 'pageId' of undefined"

**Cause:** Trying to access pageInfo before context detection completes

**Fix:** PageInfo is available in both `onPrepare()` and `onLoad()`, but ensure you're extending `PageBase` correctly.

## Backward Compatibility

The `IModule` interface is still fully supported for advanced use cases where you need manual control. You can mix and match:

* **Site class**: Continue using `IModule` (recommended)
* **Pages**: Use `PageBase` for automatic context (recommended)
* **Components**: Use `ComponentBase` for automatic context (recommended)
* **Advanced cases**: Use `IModule` when you need full manual control

## Need Help?

If you encounter issues during migration:

1. Check that all imports use `@sygnal/sse-core`
2. Verify RouteDispatcher is created once and reused
3. Ensure pages extend `PageBase` and components extend `ComponentBase`
4. Check that lifecycle methods use new names (`onPrepare`/`onLoad`)
5. Review the [sse-template](https://github.com/sygnaltech/sse-template) repository for complete examples


# Feature Roadmap

Here's what we're working on in SSE

<table><thead><tr><th width="196">Feature</th><th width="246">Notes</th><th>Priority</th></tr></thead><tbody><tr><td>Component Architecture</td><td>Supports component definition and smart code-attached componentry</td><td>High</td></tr><tr><td>File Copy</td><td>Copy non-code assets over</td><td>High</td></tr><tr><td>SA5 Integration</td><td>Integrate SA5 features like debugging </td><td>Medium</td></tr><tr><td>Augmented Build</td><td>Special build processes for assets like HTML </td><td>Low </td></tr></tbody></table>

## Component Architecture&#x20;

SSE's page routing model allows us to cleanly separate page-specific code in a very manageable way. But in some cases your code needs to be tied to a "component";

* A specially configured SwiperJS setup
* A custom component you've built such as an accordion&#x20;
* Specialized form validation&#x20;
* A fancy multi-step form&#x20;

Often, these "components" need to be reused on multiple pages, and your design team might

### Goals

* Efficient code execution, only run code when it's needed
* Code isolation, e.g. the code & CSS for a multi-step form should be distinct from the rest of your source ode&#x20;
* Reusability. Your development would should be easy to repurpose on other projects you build.&#x20;
* Webflow Component support. Take full advantage of the Webflow Team's work on components and  leverage it in every way possible to maximize the finished "smart" component.&#x20;
* Create a design paradigm that supports the possibility of multiple "component" instances per page.&#x20;

### Implementation

Our early experiments involve the use of a new custom attribute;

`sse-component` = ( component name ).

You apply this to the outer DIV of any "component" on your page, and SSE's router automatically knows to instantiate that named component, and pass it the element.  From there, it's up to the component code to decide what it wants to do, focusing within that element.&#x20;

Matching the component name to the component is currently&#x20;

## SA5 Integration

Sygnal Attributes 5 ( SA5 ) is Sygnal's open source library of Webflow&#x20;


# Component Architecture

Here's what we're working on in SSE

{% hint style="success" %}
**SHIPPED IN v2.0.0** &#x20;
{% endhint %}

## Component Architecture&#x20;

SSE's **page routing** model allows us to cleanly separate page-specific code in a very manageable way. This works when code is specific to a page or to a path like `/products/*`&#x20;

With Webflow growing push towards components, it makes sense to be able to make them "smart components" by directly attaching code to them in SSE.&#x20;

{% hint style="success" %}
This makes it possible for you to drop your standard Webflow component on any page, and SSE will only run component-specific for each instance of that component, when it encounters one.&#x20;
{% endhint %}

For example;&#x20;

* A specially configured SwiperJS setup in a hero component&#x20;
* A custom component you've built such as an accordion&#x20;
* Specialized form validation in your Contact Us form component&#x20;
* Advanced navigation mechanics in your Nav component
* A weather widget in your Footer component&#x20;
* A fancy multi-step form component&#x20;

Often, these "components" need to be reused on multiple pages, and we don't want to have to install the code for them each time you use a component on a new page.&#x20;

## Goals

* *Efficient code execution*, only run code when it's needed
* *Code isolation*, e.g. the code & CSS for a multi-step form should be separate from the rest of your page source code&#x20;
* *Reusability*. Your development would should be easy to repurpose on other projects you build.&#x20;
* *Webflow Component support*. Take full advantage of the Webflow Team's work on components and  leverage it in every way possible to maximize the finished "smart" component.&#x20;
* *Multiple instances*.  Create a design paradigm that supports the possibility of multiple "component" instances per page.&#x20;
* *Referencing.* Optionally name components for easy referencing.&#x20;
* *API support*.  Make components accessible to other code, so that you can perform certain functions;
  * e.g. a Page's code might find and reset all SwiperJS components.&#x20;

## Implementation

### Component Attributes

Our early experiments involve the use of a new custom attribute;

`sse-component` = ( component type name ).

You apply this to the outer DIV of any "component" on your page, and SSE's router automatically knows to instantiate that named component, and pass it the element.  From there, it's up to the component code to decide what it wants to do, focusing within that element.&#x20;

Matching the component name to the component is currently&#x20;

`sse-component-name`  = ( component instance name )

### Component Manager

* Is aware of all instantiated components on the page
* Can retrieve any by name
* Can retrieve a group by type&#x20;

## To Consider

* Slotted components and how this works with SSE&#x20;


# File Copy & Augmented Builds

The SA5 engine Git Repo is also useful for simple asset storage outside of what Webflow assets provides.&#x20;

* Icons
* SVG assets
* etc.&#x20;

## Augmented Builds&#x20;


# API Integration Layer

## Goals&#x20;

* Simplify calling RESTful APIs&#x20;
* Automatic error handling&#x20;
* Automatic external logging of any API errors&#x20;
* Simplify uploading files&#x20;
* Support for long-running operations ( LROs )&#x20;
  * UX messaging, progress bar, cancel...&#x20;

## Concepts

### API Contracts&#x20;

* The calling construction&#x20;
* The end goal&#x20;
* Determination of success / error&#x20;
* Success / error handling&#x20;


# Modal Management

## Goals&#x20;

* Layered modals&#x20;
* Close all&#x20;
* Modal v. Pop-up&#x20;
* Result states & Actions&#x20;

## Additional&#x20;

* Use internally for message handling&#x20;


# HSON Support

Integrated HSON support for configuration and CMS-based data-loading.&#x20;


# SA5 Integration

Add SA5 integration support&#x20;

## SA5 Integration

Sygnal Attributes 5 ( SA5 ) is Sygnal's open source library of Webflow&#x20;

<https://attr.sygnal.com&#x20>;


# Setup Github Repository

## Create new Repository&#x20;

Everything you need is there for your basic site engine.

1. Ensure that you are logged into your GitHub account.
2. Create a new repo for your site engine, based on [Sygnal's SSE Template](https://github.com/sygnaltech/sse-template)
   * [Click this link](https://github.com/new?template_name=sse-template\&template_owner=sygnaltech) to create it directly.&#x20;
3. Make your repo *public*&#x20;
   * This is necessary if you want to use Netlify free edition with an organization-owned repo. If you want your repo to be a private org repo you can use Netlify pro. &#x20;

### Public v. Private Repos

Affects your CDN hosting options as follows;&#x20;

<table><thead><tr><th width="219"></th><th width="123">Netlify Free</th><th width="117">Netlify Pro</th><th width="112">jsDelivr</th><th>NPM</th></tr></thead><tbody><tr><td></td><td>Free</td><td>$25/mo ?</td><td>Free</td><td>Free</td></tr><tr><td><strong>Organization owned</strong></td><td></td><td></td><td></td><td></td></tr><tr><td>Public repo</td><td><mark style="color:green;">Yes</mark></td><td><mark style="color:green;">Yes</mark></td><td><mark style="color:green;">Yes</mark></td><td><mark style="color:green;">Yes</mark></td></tr><tr><td>Private repo</td><td><mark style="color:red;">No</mark></td><td><mark style="color:green;">Yes</mark></td><td><mark style="color:red;">No</mark></td><td>?? </td></tr><tr><td><strong>Personally-owned</strong></td><td></td><td></td><td></td><td></td></tr><tr><td>Public repo</td><td><mark style="color:green;">Yes</mark></td><td><mark style="color:green;">Yes</mark></td><td><mark style="color:green;">Yes</mark></td><td>?? </td></tr><tr><td>Private repo</td><td><mark style="color:green;">Yes</mark></td><td><mark style="color:green;">Yes</mark></td><td><mark style="color:red;">No</mark></td><td>?? </td></tr></tbody></table>

<figure><img src="https://3849716756-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FbZeVxnZi0ThYsR4w1hXd%2Fuploads%2FWNhjHRgLvp3agwFX0TIz%2Fimage.png?alt=media&amp;token=4ffaeafb-b8c6-437c-a80e-fd3ec963bfc9" alt=""><figcaption></figcaption></figure>

## Create DEV branch

Now create a DEV branch on your repo, you'll do all of your DEV and TEST work here in a typical setup. &#x20;

<figure><img src="https://3849716756-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FbZeVxnZi0ThYsR4w1hXd%2Fuploads%2FJZvNuuisBte8priJkkzf%2Fimage.png?alt=media&amp;token=75009343-1cf9-4c85-bc0d-73d907dbc862" alt=""><figcaption></figcaption></figure>

## Initial Configuration

Create Codespace in VSCode

{% hint style="success" %}
We highly recommend that you create your codespace on the DEV branch only. This protect you against accidental deployments or main-branch commits in the Netlify configuration.&#x20;
{% endhint %}

Adjustments;

* Any basic ones desired

Create a v0.1.0 release


# Setup Netlify

Setup Netlify as your site engine code CDN

Sygnal prefers Netlify for our SSE deployments, however there are many other CDN frameworks you can utilize;&#x20;

* Coolify
* jsDelivr
* NPM

## Why Netlify?

* Generous free tier. If you use a public repo, you should not need to pay anything for the SSE setup we're using here.&#x20;
* Direct integration with Github.&#x20;
* Automatic build triggers on Github commits. Simplifies the CI/CD setup, generally avoiding the need for Github Actions in a typical SSE setup.&#x20;

Also,

* Ability to deploy from private organization-owned repos if you want to, on a paid Netlify plan.&#x20;
* Lots of additional capabilities, like serverless functions.  br

## Video Tutorial

{% embed url="<https://www.loom.com/share/d5f8ae18a98f46a5b6bad1d9522c641b>" %}

## Setup Production CDN&#x20;

Create a new site in Netlify;

* <https://app.netlify.com/start>

Connect to your repo;&#x20;

* Click Github option&#x20;

Configure your site settings;&#x20;

| Setting             | Value           | Notes                                          |
| ------------------- | --------------- | ---------------------------------------------- |
| Site name           | e.g. `mysite`   | Use something unique that represents your site |
| Branch to deploy    | `main`          |                                                |
| Base directory      | ( blank )       |                                                |
| Build command       | `npm run build` |                                                |
| Publish directory   | `dist`          |                                                |
| Functions directory | ( blank )       |                                                |

Deploy

Test it

e.g. if your site name was `mysite`, your code would be CDN delivered at;&#x20;

[https://mysite.netlify.app/index.js](https://nanistori.netlify.app/index.js)&#x20;


# jsDelivr

jsDelivr is another FREE approach, with some limitations;

* Your site repo must be public&#x20;
* You may have some challenges with code caching and `@latest`, which may require URL updates when you update your code base&#x20;

## Add to Webflow site

* Site-wide before /body&#x20;

```html
<!-- Site engine -->
<script 
  src="https://cdn.jsdelivr.net/gh/sygnaltech/REPO@0.1.0/dist/init.js" 
  dev-src="http://127.0.0.1:3000/dist/index.js"
  ></script>
```


# Setup Webflow

## Install SSE in Webflow

In **site-wide before-head**, add your script references;

{% hint style="info" %}
Make certain to adjust the `src` URLs to match your Netlify CDN.&#x20;
{% endhint %}

```html
<!-- Site Engine (SSE)
     https://engine.sygnal.com
--> 
<script 
  src="https://mysite.netlify.app/index.js"
  dev-src="http://127.0.0.1:3000/dist/index.js"
  ></script> 
```

### Notes

The SSE `<script>` element must be placed once, site-wide, in the before-HEAD custom code area.&#x20;

* `src` must point to the production CDN `index.js`&#x20;
* `test-src` ( if specified ) must point to the test CDN `index.js`&#x20;
* `dev-src` must point to your localhost served file. Typically this does not need to be changed unless e.g. you modify your `:3000` port.&#x20;

{% hint style="success" %}
No CSS link references are needed, because SSE injects these automatically where they are needed.&#x20;
{% endhint %}


# Advanced Install Notes


# Creating a Persistent Test Env

If you are working with Sygnal and using Sygnal's DevProxy, we can also setup a Persistent TEST Environment.&#x20;

## Video Tutorial

{% embed url="<https://www.loom.com/share/d5f8ae18a98f46a5b6bad1d9522c641b>" %}

## Setup Test CDN ( optional )

Create a second new site in Netlify

Connect to the same repo, with these settings;&#x20;

| Setting             | Value              | Notes                                                                       |
| ------------------- | ------------------ | --------------------------------------------------------------------------- |
| Site name           | e.g. `mysite-test` | Our convention is to use the same site name as before, affixed with `-test` |
| Branch to deploy    | `dev`              | <- note difference                                                          |
| Base directory      | ( blank )          |                                                                             |
| Build command       | `npm run build`    |                                                                             |
| Publish directory   | `dist`             |                                                                             |
| Functions directory | ( blank )          |                                                                             |

Deploy&#x20;

Test it

e.g. [https://mysite-test.netlify.app/index.js](https://nanistori.netlify.app/index.js)

## Update your Webflow Script

In site-wide before-head, add your script references;

{% hint style="info" %}
Make certain to adjust the prod and test URLs accordingly.&#x20;
{% endhint %}

```html
<!-- Site Engine (SSE)
     https://engine.sygnal.com
--> 
<script 
  src="https://mysite.netlify.app/index.js"
  test-src="https://mysite-test.netlify.app/index.js"
  dev-src="http://127.0.0.1:3000/dist/index.js"
  ></script> 
```


# Add SSE to an Existing Repo

This is an uncommon scenario, but suppose;

* You already have a Github repo for this site
* It's already Typescript-based, or contains other unrelated content like CSS and data
* You want to "convert" it to an SSE repo&#x20;

In this scenario you can "merge" SSE's template repo into your current repo, resolve any conflicts, and then gradually refactor your existing code into SSE's infrastructure.&#x20;

1. **Open the Terminal in Your Codespace**.
2. **Add the Remote Repository**: (If you haven't done this already)

   <pre class="language-sh" data-overflow="wrap"><code class="lang-sh">git remote add template-repo https://github.com/sygnaltech/sse-template.git
   </code></pre>
3. **Fetch the Remote Repository**:

   ```sh
   git fetch template-repo
   ```
4. **Merge with the `--allow-unrelated-histories` Flag**:

   ```sh
   git merge template-repo/main --allow-unrelated-histories
   ```
5. **Resolve Any Conflicts**: If there are merge conflicts, use VS Code's merge conflict resolution tools to resolve them.
6. **Commit and Push Changes**:

   ```sh
   git add .
   git commit -m "Merged template repository into current repository with unrelated histories"
   git push origin main
   ```


# Quickstart

{% hint style="warning" %}
**INCOMPLETE.**&#x20;
{% endhint %}

## 1. Create a repo based on the SSE Template

<https://github.com/sygnaltech/sse-template>

## 2. Clone your repo locally, e.g.&#x20;

```
git clone https://github.com/MYORG/MYREPO
```

{% hint style="info" %}
If it's in a Github ogranization account. The simplest, no-cost way is to make your repo public.&#x20;
{% endhint %}

## 3. Install dependencies&#x20;

```
npm run install
```

## Netlify setup&#x20;

Setup your Netlify account&#x20;

Create a project&#x20;

{% hint style="success" %}
Netlify setup is free if your repo is public, or ir it is private but in a personal Github account rather than an organization account.&#x20;
{% endhint %}

## Add to your site


# Developing with SSE

The basics of developing with Sygnal Site Engine.

## Open the Project

* Start VS Code&#x20;
* Open a GitHub codespace for your project, or check it out locally with GitHub&#x20;
* Make whatever changes you like&#x20;

## Build the Project

Open a terminal `CTRL`+`SHIFT`+`~`

Type;

```
npm run watch
```

Click the split plane button, top right of the bottom terminal pane.&#x20;

Type;&#x20;

```
npm run serve
```

{% hint style="info" %}
Now, any changes you make to the project will be immediately recompiled when you SAVE, and will be available at the URL.
{% endhint %}

<http://127.0.0.1:3000/>

## Test your work, in realitime

Typically we test our changes in the `webflow.io` staging site.  To see your code live, you can make a simple, temporary modification to your library include;

```html
<!-- Site Engine (SSE)
     https://engine.sygnal.com
--> 
<script 
  src1="https://mysite.netlify.app/index.js"
  dev- src="http://127.0.0.1:3000/dist/index.js"
  ></script> 
```

Note how the `src` attribute has been changed to `src1`, you can use anything here to suppress it.  A space has been added after the `dev-` as well, so that that `src` will now be loaded by the browser.&#x20;

Publish your `webflow.io` site ONLY.&#x20;

**Now, any code changes you make and save will immediately, and you can view them immediately on your published `webflow.io` site.**&#x20;

{% hint style="info" %}
This tag-edit approach is simple, but it is a hack.  At Sygnal we use an additional piece of infrastructure we call DevProxy which automatically makes these script changes at a persistent URL, e.g. `dev.mysite.com`.

[Talk to us](https://www.sygnal.com/contact) if you'd like this setup for your projects.&#x20;
{% endhint %}

{% hint style="warning" %}
Make sure to change the script tag back after your work, most especially before you publish your site to production.&#x20;
{% endhint %}

&#x20;


# Building & Deploying Code

In our simple configuration,&#x20;

## Deploying to TEST

Deployments to TEST are done within **VSCode**.&#x20;

* Save your work
* Build your project, `npm run build`
* Check in the files changed, and commit them back to the `dev` branch
* Push the changes to Github origin&#x20;

Wait 10 seconds for Netlify to complete the build, and your changes are live on your TEST site!

{% hint style="info" %}
This assumes you've setup a persistent TEST environment, discussed under advanced install notes in the setup guide. This is useful primarily if you want your clients to be able to review TEST changes before you bring your changes to PROD.&#x20;
{% endhint %}

## Deploying to PROD&#x20;

Deployments to PROD are done within **Github**

* Do a PR from to merge the changes from the `dev` branch to `main`&#x20;
* Merge the PR&#x20;

Wait 10 seconds for Netlify to complete the build, and your changes are live on your production site!

## Overview &#x20;

{% embed url="<https://www.loom.com/share/62b93b66fd8e4a438cb0473801731a7a>" %}


# Functional Interactions (FIX)

Functional Interactions (FIX) is a standalone version of Sygnal's SA5 Triggers, Events & Actions model, ported into SSE Code.&#x20;

It's basic purpose is to make it easy to functionally connect things in your webpage designs, using attributes.&#x20;

Examples;

* Click a button, and scroll to a position of the page&#x20;
* Change a tab, and have a slide change with it&#x20;
* Submit a form, and have custom code hand it to an API&#x20;

## Overview&#x20;

* **Trigger** - a user-initiated or system-initiated starting point for a functional interaction.
  * e.g. Clicking an element, hovering over an element, exit intent, a timer, scrolling something into view, etc.&#x20;
* **Event** - a defined event that occurs&#x20;
* **Action** -&#x20;
  * e.g.&#x20;

## Use Cases

* Mirror click. User clicks a button, and a tab also is clicked&#x20;

## Current State

The FIX foundation is implemented, and supports very basic FIX triggers, events, and actions.

Custom triggers and actions can be built at the project level.

Data is transported automatically from related attributes, and from sources such as captured for content, to provide it efficiently to the action for processing.&#x20;

## Future&#x20;

### State Tracking&#x20;

Many Webflow UI elements have state;

* Which tab is current
* Which slide is current&#x20;
* Whether a checkbox is checked or unchecked&#x20;
* Whether a Webflow form is ready for entry, or in an error or success state&#x20;

State changes themselves are often more useful as Triggers and as Actions than a simple mouse click it because there is no ambiguity.&#x20;

They also may have use as gates.  e.g. do this but only if the&#x20;

### Trigger Gates&#x20;

Criteria that must be satisfied before a trigger will fire;

* Time of day or day of week&#x20;
* A previous gate that has been passed&#x20;
* Cookie gates, i.e. time passed since a previous modal view &#x20;

### Telemetry&#x20;

via Posthog on Triggers, Events, and Actions&#x20;


# FIX Elements

|        | Events                                                                                                                                | States                                                 | Deck                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------- |
| Button | <ul><li>Click</li><li>? Right click</li><li>? Mod click</li></ul>                                                                     |                                                        |                                                                             |
| Tabs   |                                                                                                                                       | <ul><li>Tab Showing</li></ul>                          | <ul><li>First</li><li>Last</li><li>Next</li><li>Prev</li><li>None</li></ul> |
| Slider | <ul><li>Slide Changed</li><li>Slide Changing</li></ul><p>Data;<br>- User initiated v. auto<br>- Which slide from, which slide to </p> | <ul><li>Slide Changed</li><li>Slide Changing</li></ul> |                                                                             |
|        | <ul><li></li></ul>                                                                                                                    |                                                        |                                                                             |


# Code Structure

SSE encourages you to separate code into TypeScript classes that can be easily managed and organized.

These include;

* Site-level classes, which contain site-wide functionality
* Page-level classes, which contain page-specific functionality
* Component-level classes, which contain code specific to a reusable component

## Base Classes (Recommended)

**New in v2.0:** SSE provides base classes with automatic context detection. This is the recommended approach for pages and components.

### PageBase for Pages

Pages extend `PageBase` to get automatic Webflow context detection:

* **onPrepare()** - Synchronous setup during `<head>` load
* **onLoad()** - Asynchronous execution after DOM ready
* **this.pageInfo** - Automatic Webflow context (pageId, siteId, collectionId, itemSlug, etc.)

### ComponentBase for Components

Components extend `ComponentBase` to get automatic element context:

* **onPrepare()** - Synchronous setup during `<head>` load
* **onLoad()** - Asynchronous execution after DOM ready
* **this.element** - The HTMLElement the component is bound to
* **this.context** - Component metadata (name, id, dataAttributes)

## The IModule Interface (Advanced)

For advanced use cases, you can still implement the `IModule` interface directly. This gives you full control but requires manual context detection.

### Legacy Lifecycle Methods

When implementing `IModule` directly:

* **setup()** - Runs synchronously at the end of `</head>`
* **exec()** - Runs asynchronously after DOM is loaded

Let's look at how these approaches work in practice.

## Site Class

Exists at `/src/site.ts`

The Site class still uses `IModule` directly since it doesn't need automatic context detection:

```typescript
import { IModule, Page } from "@sygnal/sse-core";

export class Site implements IModule {

  constructor() {
  }

  setup() {
    // Site-wide setup code
  }

  exec() {
    console.log("Site loaded.")
  }

}
```

## Page Classes (Recommended Approach)

**New in v2.0:** Pages should extend `PageBase` for automatic Webflow context detection.

All Page classes are stored in `/src/pages` folder. By convention, Pages end in `Page`.

Here's an example using the recommended `PageBase` approach:

```typescript
import { PageBase, page } from "@sygnal/sse-core";

@page('/')
export class HomePage extends PageBase {

  protected onPrepare(): void {
    // Synchronous setup - access automatic context
    console.log('Page ID:', this.pageInfo.pageId);
    console.log('Collection:', this.pageInfo.collectionId);
  }

  protected async onLoad(): Promise<void {
    // Asynchronous execution after DOM ready
    console.log('Item Slug:', this.pageInfo.itemSlug);
    console.log("Home page loaded.");
  }

}
```

### Available Page Context

When using `PageBase`, you automatically get access to:

* `this.pageInfo.path` - Current page path
* `this.pageInfo.pageId` - Webflow page ID
* `this.pageInfo.siteId` - Webflow site ID
* `this.pageInfo.collectionId` - CMS collection ID (if applicable)
* `this.pageInfo.itemId` - CMS item ID (if applicable)
* `this.pageInfo.itemSlug` - CMS item slug (if applicable)
* `this.pageInfo.queryParams` - URL query parameters
* `this.pageInfo.hash` - URL hash
* `this.pageInfo.domain` - Webflow domain
* `this.pageInfo.lang` - Page language

## Component Classes (Recommended Approach)

**New in v2.0:** Components should extend `ComponentBase` for automatic element context.

All Component classes are stored in `/src/components` folder. By convention, Components end in `Component`.

Here's an example using the recommended `ComponentBase` approach:

```typescript
import { ComponentBase, component, PageBase } from "@sygnal/sse-core";

@component('navigation')
export class NavigationComponent extends ComponentBase {

  protected onPrepare(): void {
    // Access element and context automatically
    console.log('Component:', this.context.name);
    console.log('Element:', this.element);
  }

  protected async onLoad(): Promise<void> {
    // Access current page info if needed
    const page = PageBase.getCurrentPage();
    if (page) {
      const info = page.getPageInfo();
      console.log('On page:', info.pageId);
    }

    // Add event listeners
    this.element.addEventListener('click', () => {
      console.log('Navigation clicked');
    });
  }

}
```

Use the public `getPageInfo()` accessor (the `pageInfo` property is protected) whenever a component needs Webflow page context.

### Available Component Context

When using `ComponentBase`, you automatically get access to:

* `this.element` - The HTMLElement the component is bound to
* `this.context.name` - Component name from `data-component` attribute
* `this.context.id` - Component ID from `data-component-id` attribute
* `this.context.dataAttributes` - All data-\* attributes on the element

## Usage Notes

Execution order:

* Site code executes on all pages first (both `setup()` and `exec()`)
* Page code executes second (both `onPrepare()` and `onLoad()`)
* Component code executes last (both `onPrepare()` and `onLoad()`)

{% hint style="success" %}
Components are fully implemented in v2.0 with automatic discovery and initialization!
{% endhint %}


# Page Router

The Page Router handles *page-specific* code execution, and is defined in `routes.ts`.

## Automatic Route Discovery (v2.0+)

**New in v2.0:** Pages are automatically discovered using the `@page` decorator. No manual route registration needed!

### Using the @page Decorator

Simply decorate your page class with `@page` to auto-register it:

```typescript
import { PageBase, page } from "@sygnal/sse-core";

@page('/')
export class HomePage extends PageBase {
  protected onPrepare(): void {
    console.log('Home page preparing...');
  }

  protected async onLoad(): Promise<void> {
    console.log('Home page loaded');
  }
}
```

### Wildcard Routes

Use `*` for dynamic paths like CMS collections:

```typescript
import { PageBase, page } from "@sygnal/sse-core";

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

### Multiple Routes Per Page

Stack multiple `@page` decorators to handle multiple routes with one class:

```typescript
import { PageBase, page } from "@sygnal/sse-core";

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

### Route Registration

In `routes.ts`, import your pages to trigger the decorators, then use `getAllPages()`:

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

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

export const routeDispatcher = (): RouteDispatcher => {
    const dispatcher = new RouteDispatcher(Site);
    dispatcher.routes = getAllPages();  // Auto-populated!
    return dispatcher;
}
```

## Manual Route Registration (Legacy)

You can still manually register routes if needed:

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

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

## How this Works

* Site code will be executed first.
* A visit to the home page at `/` will execute the code in our `HomePage` class.
* A visit to any blog page, e.g. `/blog/2024-in-review` or `/blog/ai-futures-2025` will match our `/blog/*` route and execute the code in our `BlogPage` class.
* A visit to any other page, such as `/about` will execute nothing, because there is no route defined for it.
* A visit to `/blog` by itself, which might be a blog directory page, will execute nothing, because it does not match any patterns.
  * If you wanted `/blog` to also execute your `BlogPage` code, you could simply add another route for ``'/blog`: BlogPage`` .
* Each route can execute only one Page class.
* Each route must be unique, but you can add as many routes as you like.


# Components

SSE's page routing model allows us to cleanly separate page-specific code in a very manageable way. But in some cases your code needs to be tied to a "component" rather than a page or path.

{% hint style="info" %}
**Component** is a significant term in Webflow, but SSE "components" aren't strictly tied to Webflow Components. We'll be discussing both here- to distinguish...

* "Components" (capitalized) will refer to Webflow Components.
* "components" (lowercase) will refer to SSE components.
  {% endhint %}

These are some examples of "components" that might use code:

* A specially configured SwiperJS setup
* A custom component you've built such as an accordion
* Specialized form validation
* A fancy multi-step form
* A cool color-picker
* Also, any Webflow Component that needs code attached to its functionality

Often, these "components" need to be reused on multiple pages, and your design team might for example duplicate a quiz or a contact form to another page at any time- ideally the dev team would not need to do *anything* for that component to automatically work on that new page.

## Goals

* Efficient code execution, only run code when it's needed
* Code isolation, e.g. the code & CSS for a multi-step form should be distinct from the rest of your source code
* Reusability. Your development work should be easy to repurpose on other projects you build.
* Webflow Component support. Take full advantage of the Webflow Team's work on components and leverage it in every way possible to maximize the finished "smart" component.
* Create a design paradigm that supports the possibility of multiple "component" instances per page.

## Implementation (v2.0+)

{% hint style="success" %}
**New in v2.0:** Components are fully implemented with automatic discovery, ComponentBase class, and automatic context detection!
{% endhint %}

### Creating a Component

Components extend `ComponentBase` for automatic element and context detection:

```typescript
import { ComponentBase, component } from '@sygnal/sse-core';

@component('accordion')
export class AccordionComponent extends ComponentBase {

  protected onPrepare(): void {
    // Synchronous setup
    // this.element and this.context automatically available
    console.log('Accordion component:', this.context.name);
  }

  protected async onLoad(): Promise<void> {
    // Asynchronous execution
    const items = this.element.querySelectorAll('.accordion-item');

    items.forEach(item => {
      item.addEventListener('click', () => {
        item.classList.toggle('open');
      });
    });
  }
}
```

### Using Components in Webflow

To use a component, add the `data-component` attribute to any element in Webflow:

1. Select the element in Webflow Designer
2. Add a custom attribute: `data-component` = `accordion`
3. The component name must match the name in the `@component` decorator

```html
<div data-component="accordion" class="accordion-wrapper">
  <!-- Your accordion HTML -->
</div>
```

### Multiple Component Instances

Components automatically support multiple instances on the same page. Each instance gets its own separate class instance:

```html
<!-- First accordion -->
<div data-component="accordion" data-component-id="main-faq">
  <!-- FAQ content -->
</div>

<!-- Second accordion -->
<div data-component="accordion" data-component-id="secondary-info">
  <!-- Info content -->
</div>
```

Both will initialize independently with their own `AccordionComponent` instance.

## Automatic Context Detection

When extending `ComponentBase`, you automatically get:

### this.element

The HTMLElement the component is bound to:

```typescript
protected async onLoad(): Promise<void> {
  // Direct access to the component's root element
  const children = this.element.querySelectorAll('.child-item');
  this.element.addEventListener('click', () => {
    console.log('Component clicked');
  });
}
```

### this.context

Component metadata automatically extracted:

```typescript
protected onPrepare(): void {
  console.log(this.context.name);           // 'accordion'
  console.log(this.context.id);             // 'main-faq'
  console.log(this.context.dataAttributes); // All data-* attributes
}
```

Available context properties:

* `this.context.name` - Component name from `data-component`
* `this.context.id` - Optional ID from `data-component-id`
* `this.context.dataAttributes` - All data-\* attributes as key-value pairs

## Accessing Page Information from Components

**New in v2.0:** Components can access the current page via the singleton pattern:

```typescript
import { ComponentBase, component, PageBase } from '@sygnal/sse-core';

@component('navigation')
export class NavigationComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    // Access current page info
    const page = PageBase.getCurrentPage();

    if (page) {
      const info = page.getPageInfo();
      console.log('Current page ID:', info.pageId);
      console.log('Collection ID:', info.collectionId);
      console.log('Item slug:', info.itemSlug);

      // Adjust navigation based on current page
      if (info.collectionId === 'blog') {
        this.element.classList.add('blog-nav');
      }
    }
  }
}
```

Use the public `getPageInfo()` accessor (the `pageInfo` property is protected) whenever a component reads Webflow page context.

## Component Discovery and Registration

**New in v2.0:** Components are automatically discovered using the `@component` decorator!

### Automatic Discovery

Simply decorate your component class and import it:

```typescript
// src/components/accordion.ts
import { ComponentBase, component } from '@sygnal/sse-core';

@component('accordion')
export class AccordionComponent extends ComponentBase {
  // Implementation
}
```

### Register in routes.ts

Import your component files to trigger decorator registration:

```typescript
// src/routes.ts
import { RouteDispatcher, getAllPages } from "@sygnal/sse-core";
import { Site } from "./site";

// Import pages
import "./pages/home";
import "./pages/blog";

// Import components to register them
import "./components/accordion";
import "./components/navigation";
import "./components/form-validator";

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

That's it! SSE automatically finds all `data-component` attributes in your HTML and initializes the matching components.

## Component Lifecycle

Components follow the same lifecycle as pages:

1. **onPrepare()** - Runs synchronously during `<head>` load
   * Use for quick setup that doesn't require DOM manipulation
   * Context and element are available
2. **onLoad()** - Runs asynchronously after DOM is ready
   * Use for event listeners, DOM queries, async operations
   * Full DOM access guaranteed

```typescript
@component('my-component')
export class MyComponent extends ComponentBase {

  protected onPrepare(): void {
    // Quick synchronous setup
    console.log('Component preparing:', this.context.name);
  }

  protected async onLoad(): Promise<void> {
    // Async operations, DOM manipulation
    await this.loadData();
    this.attachEventListeners();
  }

  private async loadData() {
    // Fetch data, etc.
  }

  private attachEventListeners() {
    this.element.addEventListener('click', () => {
      // Handle click
    });
  }
}
```

## Data Attributes for Configuration

Use data attributes to configure component behavior:

```html
<div
  data-component="slider"
  data-component-id="hero-slider"
  data-autoplay="true"
  data-speed="3000"
  data-loop="true"
>
  <!-- Slider content -->
</div>
```

Access in your component:

```typescript
@component('slider')
export class SliderComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    // Access configuration from data attributes
    const config = this.context.dataAttributes;

    const autoplay = config['autoplay'] === 'true';
    const speed = parseInt(config['speed'] || '2000');
    const loop = config['loop'] === 'true';

    this.initializeSlider({ autoplay, speed, loop });
  }

  private initializeSlider(config: any) {
    // Initialize with config
  }
}
```

## Best Practices

1. **One component class per file** - Keep components organized in `/src/components/`
2. **Use descriptive names** - Component names should be clear: `accordion`, `multi-step-form`, `image-gallery`
3. **Scope your queries** - Always query within `this.element` to avoid conflicts:

   ```typescript
   // Good
   const items = this.element.querySelectorAll('.item');

   // Bad - might affect other components
   const items = document.querySelectorAll('.item');
   ```
4. **Clean up resources** - Remove event listeners if component is destroyed
5. **Use data attributes for configuration** - Keep components flexible and reusable

## Example: Complete Component

Here's a complete example of a tabs component:

```typescript
import { ComponentBase, component } from '@sygnal/sse-core';

@component('tabs')
export class TabsComponent extends ComponentBase {

  protected async onLoad(): Promise<void> {
    const tabButtons = this.element.querySelectorAll('[data-tab-button]');
    const tabPanes = this.element.querySelectorAll('[data-tab-pane]');

    tabButtons.forEach((button, index) => {
      button.addEventListener('click', () => {
        // Remove active from all
        tabButtons.forEach(btn => btn.classList.remove('active'));
        tabPanes.forEach(pane => pane.classList.remove('active'));

        // Add active to clicked
        button.classList.add('active');
        tabPanes[index]?.classList.add('active');
      });
    });

    // Activate first tab by default
    tabButtons[0]?.classList.add('active');
    tabPanes[0]?.classList.add('active');
  }
}
```

Use in Webflow:

```html
<div data-component="tabs" class="tabs-wrapper">
  <div class="tab-buttons">
    <button data-tab-button>Tab 1</button>
    <button data-tab-button>Tab 2</button>
    <button data-tab-button>Tab 3</button>
  </div>
  <div class="tab-content">
    <div data-tab-pane>Content 1</div>
    <div data-tab-pane>Content 2</div>
    <div data-tab-pane>Content 3</div>
  </div>
</div>
```


# Components Future Notes

## Naming & Routing

We are considering the possibility of a dual-routing architecture.

The component class itself might have a component name built into it using a TypeScript decorator, like so

### Concept code&#x20;

```
function Component(name: string) {
    return function (constructor: Function) {
        Reflect.defineMetadata('componentName', name, constructor);
    }
}
```

```
@Component('my-component')
class MyComponent {
    constructor() {
        console.log('MyComponent initialized');
    }
}

@Component('another-component')
class AnotherComponent {
    constructor() {
        console.log('AnotherComponent initialized');
    }
}

```

```
import 'reflect-metadata';

function createComponentFromAttribute(attrValue: string): any {
    const components = [MyComponent, AnotherComponent];

    for (const component of components) {
        const name = Reflect.getMetadata('componentName', component);
        if (name === attrValue) {
            return new component();
        }
    }

    throw new Error(`Component with name "${attrValue}" not found.`);
}

// Usage
const component = createComponentFromAttribute('my-component'); // Instantiates `MyComponent`

```

```
function initializeComponents() {
    const elements = document.querySelectorAll('[data-component]');
    elements.forEach(elem => {
        const componentName = elem.getAttribute('data-component');
        if (componentName) {
            const componentInstance = createComponentFromAttribute(componentName);
            if (componentInstance) {
                console.log(`${componentName} component initialized`, componentInstance);
            }
        }
    });
}

```


# Source Structure & Key Files

## Source Code

### index.ts

Main entry point.

* Initializes SSE&#x20;

### site.ts

Site-wide code that will execute on all pages.&#x20;

### site.scss

Site-wide CSS that will be included on all pages. &#x20;

### route.ts

Routes to handle for specific pages or site sections, like `/contact` or `/blog/*`&#x20;

This maps pages/paths to specific SSE page handlers which do page-specific things.&#x20;

### version.ts

Site engine version number.

Increment this on every publish.&#x20;

### page/... .ts

Page-specific code as triggered by `route.ts`.&#x20;


# Utilities

## debug.ts

## util.ts


# 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:

```typescript
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:

```typescript
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();
});
```

{% hint style="danger" %}
**Critical:** The dispatcher must be created once and reused. Creating multiple instances causes data loss between setup and execution phases.
{% endhint %}

## Route Mapping

### Automatic Route Discovery (Recommended)

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

```typescript
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:

```typescript
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:

```typescript
@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:

  ```typescript
  @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

```typescript
// 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

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

## Instance Persistence (Critical Fix)

{% hint style="danger" %}
**v2.0.0 Critical Fix:** RouteDispatcher must be created once and reused to prevent data loss.
{% endhint %}

### Wrong Approach (Data Loss)

```typescript
// ❌ 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)

```typescript
// ✅ 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:

```typescript
// 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:

```typescript
@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:

```typescript
@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:**

```typescript
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:**

```typescript
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:**

```typescript
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
  }
}
```


# Infrastructure

## routeDispatcher.ts

## debug.ts


# Usage Notes

## version.ts

## Deploying

## Route Dispatcher

## Site


# SCSS

{% hint style="warning" %}
Docs under development.
{% endhint %}

Have custom CSS you need to include in your site and pages?

You can use SASS now and directly compile it into your project.&#x20;

## Adding SASS

Site-wide

Page-specific

## Compiling SASS

Live SASS Compiler extension VSCode

## Including SASS in your Site


# Useful Library Additions

## JS-Cookie

```bash
npm install js-cookie
```

```typescript
import Cookies from 'js-cookie';
```

## GSAP


# Adding Libraries

One of the huge advantages of this setup is that you can now easily tap into a huge range of 3rd party library functionality, and it will all be bundled directly into your Site Engine codebase.

For example;&#x20;

## GSAP

```
npm install gsap
```

## JS Cookie

```
npm install js-cookie
```

## Luxon

For any form of Datetime math you need to do in your site.&#x20;

## FlatPickr

For a date-entry UI&#x20;


# Luxon

```
npm install luxon
npm install --save-dev @types/luxon
```


# Cookies

```bash
npm i --save-dev @types/js-cookie
```


# Core Libraries

These are included in the base Engine configuration.

## JS Cookie

<https://github.com/js-cookie/js-cookie>

## GSAP

<https://gsap.com/>

## SA5

Future direct integration.&#x20;


# Extending Capabilities

## Luxon

For date time formatting and conversion.&#x20;

## Howler

For audio playback.

## Handlebars ?

For templatting&#x20;

## Alpine.js


# Best Practices

## Use Classes

Sygnal uses TypeScript classes extensively in our model because they prove a clean, concise infrastructure.&#x20;

## Use Libraries

Avoid JS Date Math

Use Luxon

## Design for Monitorability

### Logging

## Avoid Classes as DOM Selectors

Use attributes instead, more versatility for your design team&#x20;


# Sygnal DevTools

{% hint style="warning" %}
**NOT YET AVAILABLE**&#x20;

This is currently in internal testing as of 2025-Nov.&#x20;
{% endhint %}

For those who do not want to use a full DevProxy setup, Sygnal has a Chrome browser extension to enable environment-switching within a single origin.&#x20;

## What it does&#x20;

* When visiting your standard Webflow staging or production sites, you can switch the codebase to DEV or TEST temporarily for your local machine only.&#x20;

## Setup

* Install the browser extension, once available&#x20;
* Utilize Sygnal's site engine code referencing structure-&#x20;


# Unit Testing

## Install Jest

In VSCode from a terminal;&#x20;

```
npm install --save-dev jest ts-jest @types/jest
```

## jest.config.js

Create a `jest.config.js` file in your project root:

```javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};
```

## tsconfig.json

**Update `tsconfig.json`:**

Ensure your `tsconfig.json` is set up to handle Jest and TypeScript. Add the following configuration if it's not already present:

```json
{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["jest"]
  }
}
```

## package.json

Add a script to your `package.json` to run the tests:

```
"scripts": {
  "test": "jest"
}
```

## # Setup Tests

### Create Tests directory

Create a `tests` directory in your project in the project root, adjacent to your `src` directory.

### Create a Test

Tests are TypeScript files that end in `.test.ts`. &#x20;

Here's an example;

```typescript
import { MaternityCalc } from '../src/maternityCalc';

describe('MaternityCalc', () => {
  const edd = new Date('2024-06-30');
  const maternityCalc = new MaternityCalc(edd);

  test('should calculate LMP date correctly', () => {
    const expectedLmpDate = new Date('2023-09-24');
    expect(maternityCalc.lmpDate.toISOString().split('T')[0]).toBe(expectedLmpDate.toISOString().split('T')[0]);
  });

  test('should calculate correct dayOf', () => {
    // You need to adjust the test depending on the current date.
    // Assuming today is 2024-06-30 for this test:
    jest.setSystemTime(new Date('2024-06-30'));
    expect(maternityCalc.dayOf).toBe(280);
  });

  test('should calculate correct weekOf', () => {
    // Assuming today is 2024-06-30 for this test:
    jest.setSystemTime(new Date('2024-06-30'));
    expect(maternityCalc.weekOf).toBe(41); // 280 days is 40 weeks, plus 1 for 1-based index
  });

  test('should create instance from LMP date correctly', () => {
    const lmp = new Date('2023-09-24');
    const instance = MaternityCalc.createFromLMP(lmp);
    const expectedEdd = new Date('2024-06-30');
    expect(instance._edd.toISOString().split('T')[0]).toBe(expectedEdd.toISOString().split('T')[0]);
  });
});

```

## Run Tests

```
npm test
```


# Using Google Sheets as a Unit Test Data Source

* Create the Google sheet
* Column headings
* Make it readable by anyone with the link
  * Security ?&#x20;

e.g.

{% code overflow="wrap" %}

```
https://docs.google.com/spreadsheets/d/1DREayEscT7YyWCNQQLSkHjeVFi5bON1KODZxTIJQEnk/export?format=csv
```

{% endcode %}

## Advanced Sheet Setup

Sample data sheet&#x20;

Based on <https://docs.google.com/spreadsheets/d/1DREayEscT7YyWCNQQLSkHjeVFi5bON1KODZxTIJQEnk/export?format=csv>

ARRAYFORMULAS

Skip a row&#x20;

RANDOM data


# Datetime & Timezone Tests

Code that uses the current system datetime needs special testing to validate your date math. In Unit Testing, there are two features we use regularly for this.

{% hint style="info" %}
Javascript's Datetime libraries are very difficult to work with effectively. We highly recommend that you use luxon instead.&#x20;
{% endhint %}

## System Clock Changes

When your code uses the browser's system clock for calculations, such as countdown / time-until-event calculations, you can use Jest's system clock change capabilities.&#x20;

## Timezone Changes

Timezone changes can also be made prior to the test being run, for example;

```
TZ=Pacific/Auckland npm test -- --silent=false
```


# Best Practices

Isolate your code&#x20;


# Page 1

```
npm test -- --silent=false
```


# What is Devproxy?

**Sygnal Devproxy is part of our development infrastructure. Its basic function is to make it very easy for the development and testing teams to run working code against a live Webflow-hosted site.**

In brief what Devproxy does is to provide special access points to your site, such as `dev.mysite.com` and `test.mysite.com`, that your team can use. When accessing these special versions;

* You can access the full site as normal.&#x20;
* Content generally is sourced from your `webflow.io` staged site.&#x20;
* Specific code, in particular Sygnal Site Engine, is swapped to use either a DEV or TEST version of the code.

This allows you and your clients to fully test code before bringing it live.

## Development Setup

Typically Sygnal's team uses this setup;

* All coding is done in TypeScript and SASS, using Sygnal Site Engine as a template and infrastructure
* All code is stored in a Github repo
  * Repo is generally public, but can be private with paid Netlify ( see below )
  * With 2 or 3 branches, e.g. `dev` `test` and `main` which is production.&#x20;
* We typically use Github codespaces&#x20;
* No Github actions needed
* Two sites are setup in Netlify, one for TEST, one for PROD.
  * Both are linked to the repo, to their respective branches
  * Both are setup to auto-update as new commits happen to those branches, making the process hands-free&#x20;
    * TS compiling is automatic&#x20;
  *

## Workflow

A general simplified ( small team ) workflow looks like this.

### Development

* Developers do programming on their local systems, with the JS and CSS code served from localhost.&#x20;
* They push and pull from the `dev` branch in the Github repo.&#x20;
* While developing they can make changes to the site and publish to `webflow.io` staging ( only ).&#x20;
* To see the code and site changes in realtime, they view the site at `dev.mysite.com`. Devproxy combines the localhost-served code into the staged site HTML and you have a full site experience.

### Testing

In a large-team Devproxy configuration; &#x20;

* When the developers are ready to show changes to the client or testing team, they can then merge the Github `dev` branch into `test`&#x20;
  * This `test` commit automatically gets picked up by Netlify, and published to the TEST code server
* This automatically recompiles the Typescript&#x20;
* Clients and testing team then test the site fully
  * The underlying HTML and CSS are typically sources from `webflow.io`, so that design changes coordinate with code changes&#x20;

For the small-team Devproxy configuration, all commits to the `dev` branch are

### Production

*

In general;

* DEVs push and pull to the `dev` branch
* When an RC is ready, the team lead merges the `dev` branch into `test`&#x20;
  * This `test` commit automatically gets picked up by Netlify, and published to the TEST code server
* Testing team evaluates it using `test.mysite.com`
* When a release is confirmed, the release manager merges the `test` branch into `main`&#x20;
  * This `main` commit automatically gets picked up by Netlify, and published to the PROD code server
  * At the same time, any changes in Webflow would also be published so that the new HTML/CSS design changes are synchronized with the PROD code release&#x20;

## FAQs

**Does Devproxy only work with Webflow-hosted sites?**

No, you could export your site elsewhere.

**Does Devproxy require the webflow\.io staging site be published?**

Yes. Most common Devproxy configurations are based on the `webflow.io` site, so that you can coordinate page design changes with code changes.&#x20;

**Does Devproxy only work with Webflow?**

No, Devproxy could work with any website, but its design is specifically intended to address limitations of developing with Webflow, and to work with Webflow's specific HTML generation.&#x20;

**Can Devproxy sites be indexed in Google?**&#x20;

Devproxy always includes a site-wide robots.txt disallow rule to ensure that your DEV and TEST sites to not get indexed by search engines.&#x20;


# Devproxy Setup

{% hint style="warning" %}
Currently **Sygnal DevProxy** can only be installed by Sygnal. \
[Contact us](https://www.sygnal.com/contact) if you are interested in a build for your projects.&#x20;
{% endhint %}

## Platform

* Cloudflare
  * Free account works fine

## Installation

Notes on installation into a new Cloudflare account.

```
cd webflow-dev-proxy
```

Add env to `wrangler.toml`;&#x20;

```
[env.ENV]
name = "webflow-dev-proxy"
account_id = "CLOUDFLARE_ACCOUNT_ID"
kv_namespaces = [
#  { binding = "DEVPROXY", id = "CLOUDFLARE_KV_STORE_ID" }
]
```

```
wrangler deploy --env ENV
```

In Cloudflare;

Create new KV store;

```
DEVPROXY
```

Copy ID to `wrangler.toml`

Uncomment binding

Re-deploy

Add key

## Cloudflare Config

### Account-Level Config

Workers;&#x20;

* Install `webflow-dev-proxy` worker in account

KV;&#x20;

* In `CONFIG` add the following entry;&#x20;
  * Key - e.g. `luxradiology.co.nz:devproxy`&#x20;
  * Value - e.g. -&#x20;

```
{ "version": 1, "origin": "https://lux-radiology.webflow.io/" }
```

### Site-Level Config

Navigate to the Website page for your domain in Cloudflare.&#x20;

DNS;

| Type | Name | Content   | Proxy status |
| ---- | ---- | --------- | ------------ |
| A    | dev  | 75.2.70.5 | Proxied      |
| A    | test | 75.2.70.5 | Proxied      |

### Cloudflare Worker Routes

Navigate to the Website page for your domain in Cloudflare

Click Worker Routes&#x20;

| Route                | Worker            |
| -------------------- | ----------------- |
| `*dev.mysite.com/*`  | webflow-dev-proxy |
| `*test.mysite.com/*` | webflow-dev-proxy |

### DEVPROXY

key - dev.mysite.com

value - mysite.webflow\.io&#x20;


# Cloudflare Setup

{% hint style="warning" %}
Currently **Sygnal DevProxy** can only be installed by Sygnal. \
[Contact us](https://www.sygnal.com/contact) if you are interested in a build for your projects.&#x20;
{% endhint %}

## Platform

* Cloudflare
  * Free account works fine

## Installation

Notes on installation into a new Cloudflare account.

```
cd webflow-dev-proxy
```

Add env to `wrangler.toml`;&#x20;

```
[env.ENV]
name = "webflow-dev-proxy"
account_id = "CLOUDFLARE_ACCOUNT_ID"
kv_namespaces = [
#  { binding = "DEVPROXY", id = "CLOUDFLARE_KV_STORE_ID" }
]
```

```
wrangler deploy --env ENV
```

In Cloudflare;

Create new KV store;

```
DEVPROXY
```

Copy ID to `wrangler.toml`

Uncomment binding

Re-deploy

Add key

## Cloudflare Config

### Account-Level Config

Workers;&#x20;

* Install `webflow-dev-proxy` worker in account

KV;&#x20;

* In `CONFIG` add the following entry;&#x20;
  * Key - e.g. `luxradiology.co.nz:devproxy`&#x20;
  * Value - e.g. -&#x20;

```
{ "version": 1, "origin": "https://lux-radiology.webflow.io/" }
```

### Site-Level Config

Navigate to the Website page for your domain in Cloudflare.&#x20;

DNS;

| Type | Name | Content   | Proxy status |
| ---- | ---- | --------- | ------------ |
| A    | dev  | 75.2.70.5 | Proxied      |
| A    | test | 75.2.70.5 | Proxied      |

### Cloudflare Worker Routes

Navigate to the Website page for your domain in Cloudflare

Click Worker Routes&#x20;

| Route                | Worker            |
| -------------------- | ----------------- |
| `*dev.mysite.com/*`  | webflow-dev-proxy |
| `*test.mysite.com/*` | webflow-dev-proxy |

### DEVPROXY

key - dev.mysite.com

value - mysite.webflow\.io&#x20;


# Webflow Site Configuration

**Sygnal DevProxy** works by swapping out `<script>` and `<link>` source references at the reverse-proxy level.

## Code Constructions&#x20;

We specify the setup we want using code constructions.

Here is a typical CSS link and script reference that you might have in your Webflow site. It happens to be for Sygnal's new SA5 Modals feature.&#x20;

{% code overflow="wrap" %}

```html
<!-- Sygnal Attributes 5 | Modals --> 
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/sygnaltech/webflow-util@5.4.1/dist/css/webflow-modal.css">
<script defer src="https://cdn.jsdelivr.net/gh/sygnaltech/webflow-util@5.4.1/dist/nocode/webflow-modal.js"></script>
```

{% endcode %}

When we want to support DEV, TEST and PROD mode distinctions here, we adjust these to code constructions that look like this;&#x20;

{% code overflow="wrap" %}

```html
<!-- Sygnal Attributes 5 | Modals --> 
<link rel="stylesheet" 
  href="https://cdn.jsdelivr.net/gh/sygnaltech/webflow-util@5.4.1/dist/css/webflow-modal.css"
  dev-href="http://127.0.0.1:4000/dist/css/webflow-modal.css"
  test-href="https://cdn.jsdelivr.net/gh/sygnaltech/webflow-util-test@5.4.1/dist/css/webflow-modal.css"
  > 
<script defer 
  src="https://cdn.jsdelivr.net/gh/sygnaltech/webflow-util@5.4.1/dist/nocode/webflow-modal.js" 
  dev-src="http://127.0.0.1:4000/dist/nocode/webflow-modal.js"
  test-src="https://cdn.jsdelivr.net/gh/sygnaltech/webflow-util-test@5.4.1/dist/nocode/webflow-modal.js"
  ></script>
```

{% endcode %}

Note the differences;

* There are now additional attributes named `dev-src` and `dev-href`.&#x20;
* And attributes named `test-src` and `test-href`.&#x20;

{% hint style="info" %}
The line breaks and indents shown here are not necessary, but they make the CC easier to read and adjust. &#x20;
{% endhint %}

## How DevProxy Works

Generally speaking, DevProxy is setup to be in one of three modes;

* DEV mode, in which source code is running on the developer's local machine or development server.
* TEST mode, in which source code is running from a test server.
* PROD mode, in which source code is running as usual from the production environment.

When DevProxy is in DEV mode;

* It will look for `<link>` elements that have a `dev-href` attribute. When found, it will overwrite the `href` attribute with the value from the `dev-href` one.&#x20;
* It will look for `<script>` elements that have a `dev-src` attribute. When found, it will overwrite the `src` attribute with the value from the `dev-src` one.&#x20;

The same process occurs when DevProxy is in TEST mode, but using the `test-` prefixed attriutes.&#x20;

In PROD mode, DevProxy does nothing, and leaves the attributes untouched.&#x20;

{% hint style="info" %}
We typically use `dev-` and `test-`, but in fact DevProxy can have any number of different modes you want, if you have a use case for it.&#x20;
{% endhint %}

## DevProxy Groups

**CC's can also be grouped into control sets, using another attribute, `dpx-group`.**

Early incarnations of DevProxy had a global on/off state, but we found that wasn't practical for more complex sites. In some cases we might need some CC's to be in a PROD state while others are in DEV and still others in TEST.&#x20;

A common example is when we are using both SSE and Sygnal Attributes 5 ( SA5 ) together in the same site, both with DevProxy CC's. In those cases, a global switch didn't work.&#x20;

This is the purpose of groups.&#x20;

When a CC contains the `dpx-group` attribute, DevProxy *adds* the ability to set the DevProxy state at the group level.

Let's suppose your page has various DevProxy CC's on it, some which have group `group1`, some which have `group2` and some which lack the `dpx-group` attribute and therefore have no group.&#x20;

DevProxy configuration will recognize 3 different settings, for a total of 9 possible configurations.&#x20;

* `group1` elements can be in DEV, TEST, or PROD mode.&#x20;
* `group2` elements can be in DEV, TEST, or PROD mode.&#x20;
* All other elements can be in DEV, TEST, or PROD mode.&#x20;

This makes it easy to isolate your development and testing to different parts of your infrastructure.&#x20;

Here a real-world example where we use this;&#x20;

These CC's have a group of `sa5`.

{% code overflow="wrap" %}

```html
<!-- Sygnal Attributes 5 | Modals --> 
<link rel="stylesheet" 
  href="https://cdn.jsdelivr.net/gh/sygnaltech/webflow-util@5.4.1/dist/css/webflow-modal.css"
  dev-href="http://127.0.0.1:4000/dist/css/webflow-modal.css"
  devproxy-group="sa5"
  > 
<script defer 
  src="https://cdn.jsdelivr.net/gh/sygnaltech/webflow-util@5.4.1/dist/nocode/webflow-modal.js" 
  dev-src="http://127.0.0.1:4000/dist/nocode/webflow-modal.js"
  devproxy-group="sa5"
  ></script>
```

{% endcode %}

While this one has a group of `sse`.

```html
<!-- Site Engine (SSE) -->
<script 
  src="https://my-site.netlify.app/index.js"
  test-src="https://my-site-test.netlify.app/index.js"
  dev-src="http://127.0.0.1:3000/dist/index.js"
  devproxy-group="sse"
  ></script> 
```

In this way, we can develop just one part in isolation, or both concurrently.&#x20;


# Configurations

## Basic Devproxy Config

<img src="https://3849716756-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FbZeVxnZi0ThYsR4w1hXd%2Fuploads%2F1Pmrhj3FScJMWfxNT19g%2Ffile.excalidraw.svg?alt=media&amp;token=3c2906e5-a68e-4d94-9b51-3a96c80be88f" alt="" class="gitbook-drawing">

## More Advanced Devproxy Config

For large teams on large projects with heavy CI/CD requirements, it's possible to separate the TEST environment.  This is most useful when;

* You have multiple devs working concurrently
* The TEST platform needs to be a vetted "release candidate" stage of content&#x20;

The basic approach here is;&#x20;

* Add a `test` branch to your Github repo
* Point the TEST code host in Netlify to the `test` branch ( instead of `dev` )
* Formalize the promotion of the `dev` to `test` branch merges&#x20;

In general;

* DEVs push and pull to the `dev` branch
* When an RC is ready, the team lead merges the `dev` branch into `test`&#x20;
  * This `test` commit automatically gets picked up by Netlify, and published to the TEST code server
* Testing team evaluates it using `test.mysite.com`
* When a release is confirmed, the release manager merges the `test` branch into `main`&#x20;
  * This `main` commit automatically gets picked up by Netlify, and published to the PROD code server
  * At the same time, any changes in Webflow would also be published so that the new HTML/CSS design changes are synchronized with the PROD code release&#x20;


# Controlling deployment flow

You can use features like Github's branch protections;

<https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches>

## Example Config

To ensure that development occurs only on the `dev` branch and to protect against accidental edits and pushes to the `test` and `main` branches, you can use GitHub's branch protection rules and enforce a pull request-based workflow. Here's how you can set it up:

#### Step-by-Step Guide

1. **Create and Push Branches**:
   * Make sure you have `dev`, `test`, and `main` branches pushed to your GitHub repository.
2. **Set Up Branch Protection Rules**:
   * Go to your GitHub repository.
   * Navigate to **Settings** > **Branches**.
   * Under **Branch protection rules**, click **Add rule**.
3. **Protect the `test` Branch**:
   * Under **Branch name pattern**, enter `test`.
   * Check the following options:
     * **Require pull request reviews before merging**: This ensures that all changes must be reviewed before being merged.
     * **Require status checks to pass before merging**: Ensure that all CI checks pass before allowing a merge.
     * **Include administrators**: Apply these rules to administrators as well.
     * **Restrict who can push to matching branches**: Select specific people or teams who can push to the `test` branch directly. Typically, you'd restrict this to maintainers or a CI/CD system.
   * Click **Create** or **Save changes**.
4. **Protect the `main` Branch**:
   * Repeat the same steps for the `main` branch.
5. **Workflow for Merging Branches**:
   * Ensure that merges from `dev` to `test` and `test` to `main` are done through pull requests (PRs).

#### Detailed Steps in GitHub

1. **Navigate to Branch Protection Rules**:
   * Go to your repository's **Settings**.
   * Click on **Branches** in the left sidebar.
   * Click on **Add rule**.
2. **Set Up Protection for `test`**:
   * Enter `test` in the **Branch name pattern** field.
   * Select the following protection settings:
     * **Require pull request reviews before merging**: Set the number of required reviewers.
     * **Require status checks to pass before merging**: Select the status checks that must pass before merging.
     * **Restrict who can push to matching branches**: Add the specific users or teams allowed to push directly (if any).
   * Click **Create** or **Save changes**.
3. **Set Up Protection for `main`**:
   * Enter `main` in the **Branch name pattern** field.
   * Select the same protection settings as for `test`.

#### Example of Branch Protection Rule

Here's an example of what the settings might look like for the `test` branch:

* **Branch name pattern**: `test`
* **Require pull request reviews before merging**:
  * [x] Require pull request reviews before merging
  * [x] Dismiss stale pull request approvals when new commits are pushed
  * [x] Require review from Code Owners (if applicable)
  * [ ] Number of required reviewers: 1 (or more, depending on your team's workflow)
* **Require status checks to pass before merging**:
  * [x] Require status checks to pass before merging
  * [x] Require branches to be up to date before merging
  * [ ] Select the specific checks that need to pass (e.g., `build`, `test`)
* **Restrict who can push to matching branches**:
  * [x] Restrict who can push to matching branches
  * [ ] Add specific teams or users who are allowed to push directly (typically, this might be a CI/CD system or a very limited number of maintainers)
* **Include administrators**: \[x]

#### Workflow Enforcement

1. **Development in `dev`**:
   * All development work is done in the `dev` branch.
   * Developers commit and push changes to `dev`.
2. **Merging to `test`**:
   * Create a pull request to merge changes from `dev` to `test`.
   * Ensure all reviewers approve the PR and all status checks pass.
   * Merge the PR to `test`.
3. **Merging to `main`**:
   * Create a pull request to merge changes from `test` to `main`.
   * Ensure all reviewers approve the PR and all status checks pass.
   * Merge the PR to `main`.

By following these steps, you can ensure that your `test` and `main` branches are protected from direct commits and that all changes go through a review and testing process before being merged. This setup enforces a disciplined workflow and helps maintain the stability of your codebase.


# Future

## Security&#x20;

Password-restricted access

IP-restricted access

Special auth through a browser extension?

## Config

Merge configs into&#x20;

## SCSS Compiling

Automate this in the build process, throughout the project&#x20;

## Clean Repo of Compiled Artifacts&#x20;

No `/dist` directory&#x20;

## Automate Provisioning?

* Github
* Creating branches
* Netlify
* Creating sites, linking to branches
* Addition of any build files to Repo&#x20;


# Source Code Repository

## Github


# Visual Studio Code

VS Code is an excellent, free IDE with tons of extensibility.

## Extensions

Our favorite extensions to install

### &#x20;#region folding for VS Code

<https://marketplace.visualstudio.com/items?itemName=maptz.regionfolder>

Collapse large code sections neatly

In TypeScript;

```
...

// #region My region

... code

// #endregion

...
```

{% hint style="info" %}
Also looks great in the right side preview window&#x20;
{% endhint %}

### Live Sass Compiler

Compile Sass or Scss to CSS at realtime.

* Create your .scss file
* Select it and click **Watch Sass** at the bottom of the page
* All changes will be automatically recompiled as you edit them &#x20;

<https://marketplace.visualstudio.com/items?itemName=glenn2223.live-sass>

Generally we want our CSS files in `/dist/css`.  Here's how to achieve that.&#x20;

### settings.json

This is the general configuration file for Live Sass Compiler, and can be found here on Windows.&#x20;

`/C:/Users/OEM/AppData/Roaming/Code/User/settings.json`.

```jsonc
{
    "git.confirmSync": false,
    "liveSassCompile.settings.autoprefix": [
    ],
    "liveSassCompile.settings.formats": [{
        "format": "expanded",
        "extensionName": ".css",
        "savePath": "/dist/css"
    }]
}
```


# Developer IDE

VS Code


# Dev Hosting

## Github Codespaces

## Localhost

## Considerations

### SSL&#x20;


# Code CDN

<table><thead><tr><th width="131">Platform</th><th width="203">Github repo access?</th><th>Build support?</th><th>Notes</th></tr></thead><tbody><tr><td>JSDelivr</td><td>Yes, public repos only</td><td>No, all files to be served must be in the repo, including the full <code>/dist</code> directory. </td><td>Over-strong caching means you really need to use @version indicators, which means version-tagged releases must be part of the TEST and PROD code deployment process. </td></tr><tr><td>Netlify</td><td>Yes, free for public repos, paid for private org-owned repos</td><td>Yes. <code>npm run build</code> </td><td>Nice support for automatic build triggering on branch commits. </td></tr><tr><td></td><td></td><td></td><td></td></tr></tbody></table>

## JSDelivr

## Netlify


# Devproxy

See above.&#x20;


# Dev Team Notes

## Test Project

<https://sygnal-site-engine.webflow.io/>

<https://webflow.com/dashboard/sites/sygnal-site-engine/code>

HEAD&#x20;

```html
<!-- Site engine -->
<script 
  src="http://127.0.0.1:3000/dist/index.js" 
  test-src="http://127.0.0.1:3000/dist/index.js"
  dev-src="http://127.0.0.1:3000/dist/index.js"
  ></script>
```

```
https://www.npmjs.com/package/@sygnal/sse
```

```
npm install @sygnal/sse
```

package.json&#x20;

```json
  "dependencies": {
    "@sygnal/sse": "^0.1.0",
    "js-cookie": "^3.0.5"
  }
```


# Reactive State Patterns

{% hint style="warning" %}
Expand on our work here.&#x20;
{% endhint %}


# Page efficiency

Avoid double load between setup and exec&#x20;

Maintain page object availabe also for client-side code&#x20;

To support page vars&#x20;


# Engine Mode

Test your code in real-time.

## Initiate Debugging

From VSCode;&#x20;

* Open a terminal window with `` CTRL+` `` &#x20;
* Switch to bash, if needed

```
npm run build
```

Open a second terminal pane, by clicking the split pane button.&#x20;

```
npm run serve
```

> Now the code is running locally

In the site, you can change the engine mode&#x20;

```
?engine.mode=dev
```

```
?engine.mode=prod
```


# Reference Project

Reference project-

<https://sygnal-site-engine.webflow.io/>

<https://sygnal-site-engine.design.webflow.com/&#x20>;


# Posthog Telemetry

Particularly for error logging in production&#x20;

Conversion logging in production&#x20;


# Functional Interaction ( FIX )

## Using Colons in Custom HTML Attribute Names

An Analysis of Browser Support and Potential Issues.&#x20;

### HTML5 Standards for Attribute Names

HTML5’s syntax does **not explicitly forbid** the colon character (`:`) in attribute names. The spec only disallows certain characters (spaces, quotes, `>`, `/`, `=`, control chars, etc.) in attribute names[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=Attributes%20have%20a%20name%20and,match%20for%20the%20attribute%27s%20name) – notably, `:` is **not** in that forbidden list. In practice, this means an attribute like `trigger:left-click` is syntactically allowed in an HTML document and will not cause a parsing error in text/html mode. However, HTML5 treats coloned names as potential XML **namespace** syntax. The standard HTML serialization supports only a fixed set of namespace-prefixed attributes (e.g. `xml:lang`, `xlink:href` in SVG/MathML) and **no others**[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=Local%20name%20%20Namespace%20,xml%3Abase)[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=No%20other%20namespaced%20attribute%20can,expressed%20in%20the%20HTML%20syntax). In other words, aside from those predefined cases, HTML5 considers attributes with `:` in their names as non-conforming (since arbitrary namespaces aren’t supported in HTML). This is why an HTML5 validator will flag a custom attribute like `foo:bar` as “not allowed” on an element. The XML Namespaces recommendation likewise advises *“authors should not use the colon in names except for namespace purposes,”* even though XML parsers will accept it as a valid characterw3.org.

**Bottom line:** By the HTML5 spec, using a colon in a custom attribute name makes the document non-standard (not valid HTML5), but it doesn’t violate the parsing rules. The colon is essentially treated as just another character in the attribute name in an HTML (non-XML) context.

### Real-World Browser Behavior

All modern browsers – Chrome (Blink engine), Firefox (Gecko), Safari (WebKit), and Edge (Blink/Chromium) – handle custom attributes with colons **gracefully in HTML documents**. In a normal HTML page (served as text/html), a non-standard attribute like `trigger:left-click="value"` will be **preserved in the DOM** of every major browser. The colon has **no special meaning** to the HTML parser beyond being part of the attribute’s name[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name). There is no automatic namespacing or special handling; the attribute will simply appear on the element as literally `trigger:left-click` (since HTML5 doesn’t apply XML namespace processing in HTML mode). Notably, MDN confirms that for HTML elements, *“the local name of an attribute is always equal to its qualified name: colons are treated as regular characters”*[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name). This means the browser does **not** split `trigger:left-click` into a prefix and local name – it remains one intact attribute name in the DOM.

Crucially, these attributes are **accessible via JavaScript** just like any other attribute. You can retrieve their values with `Element.getAttribute()`, using the exact name including the colon. For example:

```js
let val = element.getAttribute("trigger:left-click");
```

All modern browsers will return the expected string value (or `null` if the attribute isn’t present), because the attribute exists in the DOM. The standard DOM API imposes no limitation on the colon here – `getAttribute()` simply takes a name string and looks up that attribute[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#:~:text=The%20,specified%20attribute%20on%20the%20element)[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#:~:text=getAttribute). Since the colon is part of the name, it must be included in the lookup string. Similarly, you can set or remove the attribute via `setAttribute("trigger:left-click", "...")` or `removeAttribute("trigger:left-click")` with full support across browsers. In summary, **all tested modern browsers preserve custom attributes containing `:` and allow script access to them normally.** There are no differences observed between Chrome, Firefox, Safari, or modern Edge in this regard – this behavior is well-established and consistent (the `getAttribute` API has been uniformly supported for years)[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#:~:text=Baseline%20%20Widely%20available).

Importantly, using a colon does **not** trigger any HTML parsing errors or browser console warnings in a contemporary browser. The page will render and the element will carry that attribute in the DOM. This holds true as long as the document is parsed in HTML mode. (If the page were parsed as XML/XHTML, a colon in an attribute name **would** cause a fatal parsing error unless a matching namespace is defined, since XML is strict about unbound prefixes. But in HTML5’s usual mode, that’s not an issue.) In short, from a pure browser DOM perspective, `trigger:left-click` or `action:click` behave like any other non-standard attribute: they are retained and exposed to scripts.

### Potential Conflicts and Considerations

While modern browsers don’t drop or break colon-named attributes, there are a few considerations and minor “gotchas”:

* **Standards Compliance:** As mentioned, such attributes are *non-conforming* HTML5. They will fail validation (e.g. the W3C validator will complain “Attribute X not allowed on element Y…”). This doesn’t stop browsers from handling them, but it’s something to be aware of if standards compliance or XHTML compatibility is a concern[stackoverflow.com](https://stackoverflow.com/questions/16021123/is-colon-valid-in-attribute-names-for-html5#:~:text=)[stackoverflow.com](https://stackoverflow.com/questions/16021123/is-colon-valid-in-attribute-names-for-html5#:~:text=This%20is%20used%20for%20XML,per%20that%20document%20you%20referenced). Essentially, you’re venturing outside official HTML5 guidelines by not using the `data-*` prefix (or a known attribute). The HTML5 spec authors intended custom data to use `data-` attributes, partly to avoid clashing with XML naming rules.
* **XML / XHTML Mode:** In an XML-based context (XHTML or SVG as XML), a colon in an attribute name is treated as a **namespace delimiter**. If you use a custom prefix like `trigger:` in a true XML document without defining it (`xmlns:trigger`), it will be a parsing error. So, you must avoid or properly define such attributes in any XML serialization. In HTML5 (text/html), this isn’t an issue because custom namespaces are unsupported (the browser will treat `trigger:` as no namespace)[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=No%20other%20namespaced%20attribute%20can,expressed%20in%20the%20HTML%20syntax).
* **CSS Selectors and Querying:** If you need to select elements by an attribute that contains a colon, you have to be careful with CSS and DOM selectors. In CSS, the colon is a special character (used in pseudoclasses and namespace selectors), so a raw `[trigger:left-click]` selector won’t parse as intended. You would need to **escape** the colon or use a CSS namespace mechanism. For example, in a stylesheet you could write:

  ```css
  /* Escape the colon as \3A (CSS escape code for ':') */
  [trigger\3A left-click] { /* styles */ }
  ```

  Or in JS with `querySelectorAll`, escape it as `document.querySelectorAll("[trigger\\:left-click]")`. This is a quirk of the selector syntax, not a bug in the attribute itself. As long as you handle escaping, you *can* target these attributes in CSS/JS selectors (alternatively, one can use `element.hasAttribute("trigger:left-click")` in JS to check for it without dealing with selectors).
* **JavaScript DOM Interfaces:** The standard DOM **`dataset`** API (for `data-*` attributes) won’t directly help here because it only covers attributes starting with `data-`. If you attempted to include a colon in a data attribute name (e.g. `data:foo` – which is not a valid format for `data-*`), it wouldn’t map into `element.dataset` nicely. However, MDN notes that even if you violate the recommended naming rules (like including a colon in a data-\* name), the attribute still ends up in the DOM and even in `HTMLElement.dataset` (accessible via bracket notation)[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/data-*#:~:text=,as%20XML%20is%20all%20lowercase)[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/data-*#:~:text=These%20are%20recommendations,attributes%20for%20an%20%2087). In general, though, custom attributes outside the `data-` namespace won’t have any high-level API, so you’ll use `getAttribute` as discussed (which is straightforward and works uniformly).
* **No Built-in Conflicts:** Using a name like `action:click` or `trigger:left-click` does **not** conflict with any built-in HTML attributes or events. Browsers won’t mistake these for event handlers or reserved words. They are simply treated as unrelated, custom attributes. There is also no collision with pseudo-classes or other syntax in HTML itself – the colon in an attribute name is only potentially confusing to humans, not to the HTML parser.
* **Frameworks and Tools:** Be mindful that certain frameworks or libraries might strip or alter unknown attributes. For example, some templating engines or JSX/React might not permit colons in prop names, or a DOM sanitization library might remove unusual attribute names for security. Native browsers themselves do not remove the attribute, but intermediary tools could. Also, older browsers (like really old IE versions) historically had various quirks with custom tags/attrs, but all **“modern”** browsers (including Edge’s current Chromium-based version) handle it consistently.

### Conclusion

In summary, **using colons in custom HTML attribute names does not break or confuse modern browsers**. The attributes are preserved in the DOM and fully accessible via JavaScript (`getAttribute`, etc.) in Chrome, Firefox, Safari, and Edge. HTML5’s parser treats the colon as just another character in the attribute name[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name), so there’s no parsing crash or removal of the attribute in text/html mode. That said, this practice is *non-standard* from an HTML5 standpoint – such attributes are considered non-conforming because colons are officially meant for XML namespace syntax. The consensus best practice is to use data attributes (e.g. `data-trigger-left-click`) or another supported mechanism for custom data. Doing so ensures maximum compatibility and clarity. If you do use `trigger:left-click`-style attributes, be aware of the minor caveats: they will trip HTML validators and require escaping in CSS selectors, but functionally they will work in the DOM as normal. Official documentation and tests back this up: browsers must accept colon in names even if authors are discouraged from using itw3.org, and in HTML5 DOMs the coloned attributes remain as-is with no special treatment[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name). In practice, developers have successfully used such attributes, but the recommendation is to avoid them unless necessary, to stay aligned with HTML5 conventions[stackoverflow.com](https://stackoverflow.com/questions/16021123/is-colon-valid-in-attribute-names-for-html5#:~:text=This%20is%20used%20for%20XML,per%20that%20document%20you%20referenced).

**References:** Browser/DOM specs and documentation confirm these behaviors. For instance, the HTML5 spec’s syntax rules allow any characters except a few (excluding `:`) in attribute names[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=Attributes%20have%20a%20name%20and,match%20for%20the%20attribute%27s%20name), and explicitly state that arbitrary namespace-like attributes aren’t part of HTML (aside from defined ones)[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=No%20other%20namespaced%20attribute%20can,expressed%20in%20the%20HTML%20syntax). Mozilla’s MDN Web Docs note that in HTML, “colons are treated as regular characters” in attribute names[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name), and no errors occur even if such naming deviates from XML conventions[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/data-*#:~:text=These%20are%20recommendations,attributes%20for%20an%20%2087). All major browser engines implement attribute storage and retrieval uniformly, so custom attributes with colons will be retained and accessible across the board. This aligns with real-world testing and the community’s experience that while you *can* use `:` in attribute names without technical breakdowns, it should be done with caution and awareness of standards.


# Future


# Component Development

Code your components!

{% hint style="info" %}
In SSE we describe a functional UI piece as a "component." It is not necessarily the same thing as a component in Webflow terms, but it can be, and it often makes sense to build it this way for mobility.&#x20;
{% endhint %}

What is a component?

Any piece of UI that is self-contained, and likely to be reused across pages.&#x20;

Features;

* Attach code directly to your component, rather than do your page
* Automatically determine where your component is use, and load the code only when it is needed
* Provide the same support for setup() and exec() that pages have  &#x20;
* Support component isolation, so that multiple instances do not complicate code&#x20;

## Usage Notes

Wrap your component in a div

Assign it a custom attribute of `sse-component` = ( name )&#x20;

It will be identified by SSE and then initialized&#x20;

## Future

Auto infer the class to run based on the component name?&#x20;


# Reactive State Management

Central data models

Can be instanced at the page level, site level, component level&#x20;

Allow automatic UI updates tied to the reactive component

* Set data    sse-data-item   model.attribute  &#x20;
* Element group conditional selection   sse-element-group   model.attriute &#x20;

## Goals

* Push logic inferentially into the data structure
* Make it easy to update the UX directly from data changes&#x20;

## Future

* Bi-directional?  Avoid cyclical loops&#x20;
* Change an input value...
  * Data updates
  * Creates other data updates
  * Updates other things&#x20;
* State arrays?
  * Quiz cards...&#x20;


# Expand Script Loading

## Service model&#x20;

Rename IRouteHandler to IModule&#x20;

Breaking change 0.2.0

Allow ID-&#x20;

```
<script id="timelyScript" src="//book.gettimely.com/widget/book-button-v1.5.js"></script>
```

Page.Head.loadScript

top, bottom

loadScript ( body, head )

async

type

etc

Style

Consider additional safeties

```typescript
  static loadTimelyScript(): Promise<void> {

    // Check to see if it exists? install only if needed? 



    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
//      script.src = "//book.gettimely.com/widget/book-button-v1.3.js";
      script.src = "//book.gettimely.com/widget/book-button-v1.5.js";  
      script.id = 'timelyScript';
      script.onload = () => resolve();
      script.onerror = () => reject(new Error(`Failed to load script: ${script.src}`));
      document.head.appendChild(script);
    });
  }
```


# Devmode

Engine states

Triggered by browser extension&#x20;

## SSL Note

`thisisunsafe`

Using a third-party tool


# CI/CD Discussions

Github actions&#x20;

Controlled deployments via branch rules&#x20;

<https://vimeo.com/960644630>


# Page 2


# SSE CLI

## Manage Pages

```
sse pages add
```

* Create page from a template&#x20;
* import into routes
*

## Components

```
sse components add
```

## Reactive Variables&#x20;


# Webflow Intelligence

## Environment&#x20;

* Code Preview&#x20;
* Staging&#x20;
* Prod&#x20;

## General&#x20;

### Page type&#x20;

* Static page?
* Collection page?
* Utility page?&#x20;

## Localization&#x20;

* What locale am I on?
* What locales are there?
* What is the base locale?&#x20;
* What are the paths to this page on other locales?&#x20;


# SA5

Direct SA5 Integration


# Webflow Designer Notation

Use Emoji's and Unicode characters to prefix class names with visible indicators.

Here are some Sygnal uses regularly as class names to highlight specific items.&#x20;

|                            |                  |                 |
| -------------------------- | ---------------- | --------------- |
| 🔵 code                    |                  | Code            |
| 🟡 css                     |                  | CSS             |
| 🔴 admin                   |                  | Admin           |
| ⚪️ designer                |                  | Designer switch |
| 🡒                         | Long right arrow |                 |
| 🌟                         |                  |                 |
| ⛔️                         |                  |                 |
| ❌ ⭕️ 🛑                    |                  |                 |
| ✅ 🈯️ 💹 ❇️ ✳️ ❎           |                  |                 |
| 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 |                  |                 |
| 🟥 🟧 🟨 🟩 🟦 🟪 ⬛️ ⬜️ 🟫 |                  |                 |
| ✖️                         |                  |                 |
| ☑️                         |                  |                 |
| ⭐                          |                  |                 |


# cookie.js


# PostHog

## Installing

### Install the package;

```
npm install posthog-js
```

### Initalize site-wide in your site.ts

At the top under imports, add;

```typescript
import posthog from 'posthog-js'
```

In the setup() method, use the posthog.init code given to you;

e.g.;

{% code overflow="wrap" %}

```typescript
posthog.init('phc_YOUR_KEY', { api_host: 'https://us.i.posthog.com', person_profiles: 'identified_only' })
```

{% endcode %}

Note; you can choose to initialize only on production sites, e.g.;&#x20;

{% code overflow="wrap" %}

```typescript
// Init Posthog
// Only on non-staging pages 
if (!window.location.host.endsWith('.webflow.io')) {
  posthog.init('phc_YOUR_KEY', { api_host: 'https://us.i.posthog.com', person_profiles: 'identified_only' })
}

```

{% endcode %}

## Usage Notes

In the exec() method, you'll perform your actual tests.

### Feature Flags

Good for;

* Switching on or off specific features
* Doing a limited deployment of a feature to a smaller audience

```typescript
if (posthog.isFeatureEnabled('flag-key') ) {
    // Do something differently for this user

    // Optional: fetch the payload
    const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key')
}
```

### Experiments (A/B tests)

* Good for handling variants&#x20;

{% code overflow="wrap" %}

```typescript
if (posthog.getFeatureFlag('flag-key')  == 'variant-key') { // replace 'variant-key' with the key of your variant
    // Do something differently for this user
    
    // Optional: fetch the payload
    const matchedFlagPayload = posthog.getFeatureFlagPayload('flag-key')
}
```

{% endcode %}


# HTML5 Attribute Names

## Using Colons in Custom HTML Attribute Names

An Analysis of Browser Support and Potential Issues.&#x20;

### HTML5 Standards for Attribute Names

HTML5’s syntax does **not explicitly forbid** the colon character (`:`) in attribute names. The spec only disallows certain characters (spaces, quotes, `>`, `/`, `=`, control chars, etc.) in attribute names[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=Attributes%20have%20a%20name%20and,match%20for%20the%20attribute%27s%20name) – notably, `:` is **not** in that forbidden list. In practice, this means an attribute like `trigger:left-click` is syntactically allowed in an HTML document and will not cause a parsing error in text/html mode. However, HTML5 treats coloned names as potential XML **namespace** syntax. The standard HTML serialization supports only a fixed set of namespace-prefixed attributes (e.g. `xml:lang`, `xlink:href` in SVG/MathML) and **no others**[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=Local%20name%20%20Namespace%20,xml%3Abase)[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=No%20other%20namespaced%20attribute%20can,expressed%20in%20the%20HTML%20syntax). In other words, aside from those predefined cases, HTML5 considers attributes with `:` in their names as non-conforming (since arbitrary namespaces aren’t supported in HTML). This is why an HTML5 validator will flag a custom attribute like `foo:bar` as “not allowed” on an element. The XML Namespaces recommendation likewise advises *“authors should not use the colon in names except for namespace purposes,”* even though XML parsers will accept it as a valid characterw3.org.

**Bottom line:** By the HTML5 spec, using a colon in a custom attribute name makes the document non-standard (not valid HTML5), but it doesn’t violate the parsing rules. The colon is essentially treated as just another character in the attribute name in an HTML (non-XML) context.

### Real-World Browser Behavior

All modern browsers – Chrome (Blink engine), Firefox (Gecko), Safari (WebKit), and Edge (Blink/Chromium) – handle custom attributes with colons **gracefully in HTML documents**. In a normal HTML page (served as text/html), a non-standard attribute like `trigger:left-click="value"` will be **preserved in the DOM** of every major browser. The colon has **no special meaning** to the HTML parser beyond being part of the attribute’s name[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name). There is no automatic namespacing or special handling; the attribute will simply appear on the element as literally `trigger:left-click` (since HTML5 doesn’t apply XML namespace processing in HTML mode). Notably, MDN confirms that for HTML elements, *“the local name of an attribute is always equal to its qualified name: colons are treated as regular characters”*[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name). This means the browser does **not** split `trigger:left-click` into a prefix and local name – it remains one intact attribute name in the DOM.

Crucially, these attributes are **accessible via JavaScript** just like any other attribute. You can retrieve their values with `Element.getAttribute()`, using the exact name including the colon. For example:

```js
let val = element.getAttribute("trigger:left-click");
```

All modern browsers will return the expected string value (or `null` if the attribute isn’t present), because the attribute exists in the DOM. The standard DOM API imposes no limitation on the colon here – `getAttribute()` simply takes a name string and looks up that attribute[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#:~:text=The%20,specified%20attribute%20on%20the%20element)[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#:~:text=getAttribute). Since the colon is part of the name, it must be included in the lookup string. Similarly, you can set or remove the attribute via `setAttribute("trigger:left-click", "...")` or `removeAttribute("trigger:left-click")` with full support across browsers. In summary, **all tested modern browsers preserve custom attributes containing `:` and allow script access to them normally.** There are no differences observed between Chrome, Firefox, Safari, or modern Edge in this regard – this behavior is well-established and consistent (the `getAttribute` API has been uniformly supported for years)[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#:~:text=Baseline%20%20Widely%20available).

Importantly, using a colon does **not** trigger any HTML parsing errors or browser console warnings in a contemporary browser. The page will render and the element will carry that attribute in the DOM. This holds true as long as the document is parsed in HTML mode. (If the page were parsed as XML/XHTML, a colon in an attribute name **would** cause a fatal parsing error unless a matching namespace is defined, since XML is strict about unbound prefixes. But in HTML5’s usual mode, that’s not an issue.) In short, from a pure browser DOM perspective, `trigger:left-click` or `action:click` behave like any other non-standard attribute: they are retained and exposed to scripts.

### Potential Conflicts and Considerations

While modern browsers don’t drop or break colon-named attributes, there are a few considerations and minor “gotchas”:

* **Standards Compliance:** As mentioned, such attributes are *non-conforming* HTML5. They will fail validation (e.g. the W3C validator will complain “Attribute X not allowed on element Y…”). This doesn’t stop browsers from handling them, but it’s something to be aware of if standards compliance or XHTML compatibility is a concern[stackoverflow.com](https://stackoverflow.com/questions/16021123/is-colon-valid-in-attribute-names-for-html5#:~:text=)[stackoverflow.com](https://stackoverflow.com/questions/16021123/is-colon-valid-in-attribute-names-for-html5#:~:text=This%20is%20used%20for%20XML,per%20that%20document%20you%20referenced). Essentially, you’re venturing outside official HTML5 guidelines by not using the `data-*` prefix (or a known attribute). The HTML5 spec authors intended custom data to use `data-` attributes, partly to avoid clashing with XML naming rules.
* **XML / XHTML Mode:** In an XML-based context (XHTML or SVG as XML), a colon in an attribute name is treated as a **namespace delimiter**. If you use a custom prefix like `trigger:` in a true XML document without defining it (`xmlns:trigger`), it will be a parsing error. So, you must avoid or properly define such attributes in any XML serialization. In HTML5 (text/html), this isn’t an issue because custom namespaces are unsupported (the browser will treat `trigger:` as no namespace)[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=No%20other%20namespaced%20attribute%20can,expressed%20in%20the%20HTML%20syntax).
* **CSS Selectors and Querying:** If you need to select elements by an attribute that contains a colon, you have to be careful with CSS and DOM selectors. In CSS, the colon is a special character (used in pseudoclasses and namespace selectors), so a raw `[trigger:left-click]` selector won’t parse as intended. You would need to **escape** the colon or use a CSS namespace mechanism. For example, in a stylesheet you could write:

  ```css
  /* Escape the colon as \3A (CSS escape code for ':') */
  [trigger\3A left-click] { /* styles */ }
  ```

  Or in JS with `querySelectorAll`, escape it as `document.querySelectorAll("[trigger\\:left-click]")`. This is a quirk of the selector syntax, not a bug in the attribute itself. As long as you handle escaping, you *can* target these attributes in CSS/JS selectors (alternatively, one can use `element.hasAttribute("trigger:left-click")` in JS to check for it without dealing with selectors).
* **JavaScript DOM Interfaces:** The standard DOM **`dataset`** API (for `data-*` attributes) won’t directly help here because it only covers attributes starting with `data-`. If you attempted to include a colon in a data attribute name (e.g. `data:foo` – which is not a valid format for `data-*`), it wouldn’t map into `element.dataset` nicely. However, MDN notes that even if you violate the recommended naming rules (like including a colon in a data-\* name), the attribute still ends up in the DOM and even in `HTMLElement.dataset` (accessible via bracket notation)[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/data-*#:~:text=,as%20XML%20is%20all%20lowercase)[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/data-*#:~:text=These%20are%20recommendations,attributes%20for%20an%20%2087). In general, though, custom attributes outside the `data-` namespace won’t have any high-level API, so you’ll use `getAttribute` as discussed (which is straightforward and works uniformly).
* **No Built-in Conflicts:** Using a name like `action:click` or `trigger:left-click` does **not** conflict with any built-in HTML attributes or events. Browsers won’t mistake these for event handlers or reserved words. They are simply treated as unrelated, custom attributes. There is also no collision with pseudo-classes or other syntax in HTML itself – the colon in an attribute name is only potentially confusing to humans, not to the HTML parser.
* **Frameworks and Tools:** Be mindful that certain frameworks or libraries might strip or alter unknown attributes. For example, some templating engines or JSX/React might not permit colons in prop names, or a DOM sanitization library might remove unusual attribute names for security. Native browsers themselves do not remove the attribute, but intermediary tools could. Also, older browsers (like really old IE versions) historically had various quirks with custom tags/attrs, but all **“modern”** browsers (including Edge’s current Chromium-based version) handle it consistently.

### Conclusion

In summary, **using colons in custom HTML attribute names does not break or confuse modern browsers**. The attributes are preserved in the DOM and fully accessible via JavaScript (`getAttribute`, etc.) in Chrome, Firefox, Safari, and Edge. HTML5’s parser treats the colon as just another character in the attribute name[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name), so there’s no parsing crash or removal of the attribute in text/html mode. That said, this practice is *non-standard* from an HTML5 standpoint – such attributes are considered non-conforming because colons are officially meant for XML namespace syntax. The consensus best practice is to use data attributes (e.g. `data-trigger-left-click`) or another supported mechanism for custom data. Doing so ensures maximum compatibility and clarity. If you do use `trigger:left-click`-style attributes, be aware of the minor caveats: they will trip HTML validators and require escaping in CSS selectors, but functionally they will work in the DOM as normal. Official documentation and tests back this up: browsers must accept colon in names even if authors are discouraged from using itw3.org, and in HTML5 DOMs the coloned attributes remain as-is with no special treatment[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name). In practice, developers have successfully used such attributes, but the recommendation is to avoid them unless necessary, to stay aligned with HTML5 conventions[stackoverflow.com](https://stackoverflow.com/questions/16021123/is-colon-valid-in-attribute-names-for-html5#:~:text=This%20is%20used%20for%20XML,per%20that%20document%20you%20referenced).

**References:** Browser/DOM specs and documentation confirm these behaviors. For instance, the HTML5 spec’s syntax rules allow any characters except a few (excluding `:`) in attribute names[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=Attributes%20have%20a%20name%20and,match%20for%20the%20attribute%27s%20name), and explicitly state that arbitrary namespace-like attributes aren’t part of HTML (aside from defined ones)[w3.org](https://www.w3.org/TR/2014/REC-html5-20141028/syntax.html#:~:text=No%20other%20namespaced%20attribute%20can,expressed%20in%20the%20HTML%20syntax). Mozilla’s MDN Web Docs note that in HTML, “colons are treated as regular characters” in attribute names[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Attr/localName#:~:text=That%20means%20that%20the%20local,different%20from%20the%20qualified%20name), and no errors occur even if such naming deviates from XML conventions[developer.mozilla.org](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/data-*#:~:text=These%20are%20recommendations,attributes%20for%20an%20%2087). All major browser engines implement attribute storage and retrieval uniformly, so custom attributes with colons will be retained and accessible across the board. This aligns with real-world testing and the community’s experience that while you *can* use `:` in attribute names without technical breakdowns, it should be done with caution and awareness of standards.


