This guide is for application developers who are deciding where front-end code should live in an Assegai project.
If your question is "Where do I put my JavaScript without turning the app into a pile of browser scripts?", start here.
The core idea is simple:
- start with server-rendered HTML
- add HTMX when you want server-driven interaction
- add Web Components when a piece of UI needs browser-side behavior
- keep global one-off browser code in
public/js/main.js - keep first-party Assegai Web Components in generated
.wc.tsfiles undersrc/
By the end of this guide, you should know where front-end code lives, how to generate new components, and how to run the supported build/watch flow.
The default path for a new app
For a new project, the happy path is:
- render HTML from a
Viewor a component-backed page - generate a Web Component when a specific UI element needs client-side behavior
- run
assegai serve --devwhile you work - let Assegai inject the bundle automatically once it exists
That keeps the server in charge of the page while giving you a clean place for interactive browser code.
Where code belongs
src/Views/*.php
Use classic views when:
- the page is mostly HTML plus server data
- you do not need a feature-specific page component yet
- you want the fastest possible path from controller to HTML
src/<Feature>/...Component.php and .twig
Use component-backed pages when:
- the page belongs to a feature module
- the template, service, controller, and module should stay together
- the page still wants to be server-rendered first
public/js/main.js
Keep main.js for:
- tiny page-level DOM helpers
- third-party scripts that are not part of the Assegai Web Components pipeline
- truly global browser behavior that does not need to become a reusable custom element
Do not treat main.js as the main home for new first-party Assegai Web Components. Those belong in .wc.ts source files so the CLI can discover, bundle, and watch them.
src/**/*.wc.ts
Use .wc.ts files for:
- new custom elements generated with
assegai g wc ... - browser-side runtime files paired with generated pages or components via
--wc - reusable UI elements that should be bundled into the Assegai Web Components runtime
Create your first Web Component
Generate a standalone Web Component:
assegai g wc ui/alert
Or pair a page or component with a runtime file:
assegai g component user-card --wc
assegai g pg about --wc
That gives you a server-rendered feature plus a browser-side custom element file that participates in the first-party build flow.
A generated component is expected to render through the shadow root
Generated Web Components use the Assegai runtime and attach a shadow root automatically. A typical render method looks like this:
protected render(): void {
const name: string = this.getAttribute('name') || 'user-card';
this.shadow.innerHTML = `
<style></style>
<p>${name} works!</p>`;
}
That means:
- render into
this.shadow - keep component styles inside the shadow tree when you want local encapsulation
- think of the generated runtime as the normal Assegai Web Components shape for new components
If a component is present in the HTML but its client-side behavior never appears, the most common issue is not the shadow DOM itself. It is usually that the bundle was never built, never watched, or never injected.
Run the front-end workflow
The first-party commands are:
assegai wc:list
assegai wc:build
assegai wc:watch
wc:list answers a very useful question immediately: did the CLI actually discover the Web Components you think it should be bundling?
For development, the easiest loop is:
assegai serve --dev
That starts the PHP dev server and the Web Components watcher together.
If you prefer two separate terminals, this is still valid:
assegai serve
assegai wc:watch
For most new projects, serve --dev is the easiest starting point.
How props move from PHP into a Web Component
From Twig:
<app-user-card data-props='{{ ctx.webComponentProps({
name: user.name,
role: user.role
}) }}'></app-user-card>
From a PHP view:
<app-user-card
data-props='<?= web_component_props([
"name" => $user["name"],
"role" => $user["role"],
]) ?>'
></app-user-card>
data-props is not a special Assegai-only format. It is just JSON stored in an HTML attribute.
The helper exists because JSON inside HTML needs to be encoded and escaped safely. Quotes, apostrophes, and other characters can break the markup if you try to hand-build the string. web_component_props(...) and ctx.webComponentProps(...) do that safely and give PHP views and Twig templates one consistent way to pass data.
The runtime reads that data-props payload and hydrates the custom element in the browser.
Bundle configuration lives in assegai.json
The basic shape is:
{
"webComponents": {
"enabled": true,
"prefix": "app",
"output": "public/js/assegai-components.min.js"
}
}
Important keys:
enabledcontrols whether automatic bundle injection is activeprefixcontrols generated selectors such asapp-user-cardoutputcontrols where the browser bundle is written
If a bundle exists at the configured output path, Assegai injects it automatically into rendered HTML.
Global favicon and scripts are configured once in config/default.php
You do not need to add a favicon or site-wide scripts per page anymore.
<?php
return [
'app' => [
'title' => 'Blog API',
'favicon' => ['/favicon.ico', 'image/x-icon'],
'links' => ['/css/style.css'],
'headScriptUrls' => ['/js/main.js'],
'bodyScriptUrls' => ['/js/analytics.js'],
],
];
That configuration applies to both classic View rendering and component-backed rendered pages.
FAQ
Why are my new Web Components not rendering?
Check these in order:
- Is the custom element tag actually present in the rendered HTML?
- Does
assegai wc:listshow the component? - Did you run
assegai wc:watch,assegai wc:build, orassegai serve --dev? - Does the rendered page include the Web Components bundle?
- Are you rendering into
this.shadowin the generated runtime style?
What if my project is older and everything lives in public/js/main.js?
Keep the old browser code working first, then migrate gradually:
- Leave existing global scripts in
public/js/main.js. - Generate new custom elements with
assegai g wc ...or pair feature generation with--wc. - Put those new custom-element definitions in
.wc.tsfiles instead of appending them tomain.js. - Run
assegai wc:listand confirm the new components are discovered. - Run
assegai wc:watchorassegai serve --dev.
This lets you move onto the first-party runtime without rewriting every older browser script at once.
A good default strategy
Keep the server in charge of HTML.
Use main.js sparingly for truly global behavior. Use first-party .wc.ts files for new custom elements. Let serve --dev or wc:watch keep the bundle current while you work.
That keeps the application modular, predictable, and much easier to explain to the next person who joins the project.