The FAST adapter lets you write pages using Microsoft FAST Element 2, with full SSR support via @microsoft/fast-ssr.
pnpm create @beatzball/litro my-app --adapter fast
The client entry (app.ts) must import hydration support first:
// app.ts
import '@microsoft/fast-element/install-element-hydration.js'; // MUST be first
import '@beatzball/litro/adapter/fast/runtime';
import { routes } from './routes.generated.js';
const outlet = document.querySelector('litro-outlet');
if (outlet) outlet.routes = routes;
Pages extend LitroPage from the FAST adapter path. Use FAST's html template, css, and observable for reactive properties.
// pages/index.ts
import { FASTElement, observable, html, css } from '@microsoft/fast-element';
import { LitroPage } from '@beatzball/litro/adapter/fast/page';
import { definePageData } from '@beatzball/litro/runtime/page-data.js';
export const pageData = definePageData(async (event) => {
return { message: 'Hello from the server (FAST)!' };
});
export class HomePage extends LitroPage {
@observable override serverData: { message: string } | null = null;
override async fetchData() {
const res = await fetch('/api/hello');
return res.json();
}
}
HomePage.define({
name: 'page-home',
template: html<HomePage>`
<h1>Welcome to Litro (FAST)</h1>
<p>${x => x.serverData?.message ?? 'Loading...'}</p>
`,
styles: css`
:host { display: block; padding: 2rem; }
h1 { color: #1a1a2e; }
`,
});
export default HomePage;
ComponentClass.define({ name, template, styles }) instead of the @customElement decoratorhtml tag uses arrow function bindings (${x => x.prop}) instead of Lit's expression interpolation@observable instead of @property() / @state()Observable.defineProperty() works without decorators, which avoids jiti/esbuild compatibility issuesFAST SSR uses @microsoft/fast-ssr to render components as Declarative Shadow DOM, similar to Lit. The adapter's manifest preamble installs a DOM shim and initialises the SSR template renderer before any page modules load.
The HTML output includes a DSD polyfill inline script, same as the Lit adapter.
Unlike Lit (which uses property bindings during SSR), FAST SSR passes serverData via globalThis.__litro_ssr_page_data__. The FAST LitroPage base class reads this in connectedCallback during SSR. This is safe because Node.js SSR is single-threaded and sequential.
Same as Lit — Shadow DOM scoping via adoptedStyleSheets. Global CSS cannot pierce the shadow boundary.
The FAST adapter:
@microsoft/fast-* packages external (not inlined) to avoid dual-copy SSR issuesFAST SSR renders component templates as Declarative Shadow DOM, but not all template features produce server-side output:
${x => x.title}) — rendered correctlyrepeat() directives — rendered correctly:innerHTML property bindings — not rendered server-side. The element is emitted as an empty tag. Content appears after client-side hydration.${x => x.data ? html\...` : html`...`}) — rendered based on state at first evaluation. If serverData` isn't populated before the template evaluates, the loading/fallback branch renders.This means FAST SSG output is smaller than Lit for pages with :innerHTML bindings (e.g. comment trees, Markdown content), because that content is missing from the static HTML. The content still loads correctly after hydration, but is not available to users or search engines with JavaScript disabled.
Workaround: For content that must appear in the static HTML, use template interpolation instead of :innerHTML:
// Instead of:
html`<div :innerHTML="${rawHtml}"></div>`
// Render the HTML into the template directly:
function renderComments(comments) {
return comments.map(c => `<div class="comment">${c.text}</div>`).join('');
}
// ... then use a string binding in the parent template
When using Nitro's crawlLinks: true for SSG, FAST pages may discover fewer links than Lit or Elena pages because :innerHTML content (which may contain <a> tags) is absent from the prerendered HTML. If your pages contain links inside :innerHTML bindings, use explicit prerender.routes in your Nitro config instead of relying on crawlLinks.
@observable with jiti — Nitro's jiti loader cannot process @observable decorators. Use Observable.defineProperty() for properties that need to work during SSR. Client-side @observable works fine.@microsoft/fast-element, breaking SSR's element registry.:innerHTML not rendered in SSR — see SSR Output Fidelity above.