LentyStyle does not ship per-framework adapter packages. Every framework integration follows the same three-line pattern: your framework renders the page HTML, renderRoute() resolves the .luis sources from config and injects the SSR artifacts, and you send result.html.
One-Time Setup
Section titled “One-Time Setup”Declare route-to-source mappings once in luis.config.mjs:
import { defineLuisConfig } from '@lentystyle/core'
export default defineLuisConfig({ ssr: { // Full application pages carry their own scripts. htmlGuardMode: 'hybrid', routes: [ { match: ['/', '/blog/**'], sources: ['styles/base.luis'] }, { match: '/docs/**', sourceDirs: ['styles/docs'] }, ], },})Then create the integration once at server startup:
import { createLentySsrProdFromProjectConfig } from '@lentystyle/ssr'
const ssr = await createLentySsrProdFromProjectConfig()Express / Node HTTP
Section titled “Express / Node HTTP”import express from 'express'import { createLentySsrProdFromProjectConfig, isLentySsrError } from '@lentystyle/ssr'
const app = express()const ssr = await createLentySsrProdFromProjectConfig()
app.get('*', async (req, res, next) => { try { const html = await renderAppHtml(req) // your framework render const render = await ssr.renderRoute(req.path, html) res.type('html').send(render.result.html) } catch (error) { if (isLentySsrError(error)) { res.status(500).send(`SSR failed: ${error.code}`) return } next(error) }})Astro Middleware
Section titled “Astro Middleware”import { defineMiddleware } from 'astro:middleware'import { createLentySsrProdFromProjectConfig } from '@lentystyle/ssr'
const ssrReady = createLentySsrProdFromProjectConfig()
export const onRequest = defineMiddleware(async (context, next) => { const response = await next() if (!response.headers.get('content-type')?.includes('text/html')) { return response } const ssr = await ssrReady const render = await ssr.renderRoute(context.url.pathname, await response.text()) return new Response(render.result.html, response)})Vite Dev Server
Section titled “Vite Dev Server”// vite.config.ts plugin snippetimport { createLentySsrFromProjectConfig } from '@lentystyle/ssr'
export function lentySsrPlugin() { const ssrReady = createLentySsrFromProjectConfig() return { name: 'lenty-ssr', transformIndexHtml: { order: 'post', async handler(html, context) { const ssr = await ssrReady const result = await ssr.renderRoute(context.path, html) return result.html }, }, }}Static Output Instead?
Section titled “Static Output Instead?”If your framework emits static HTML files (Astro static, SSG builds), you do not need request-time integration at all — run the zero-config CLI over the build output:
npx lenty-ssrSee Overview for the runner details.
After the Response: Client-Side DOM Changes
Section titled “After the Response: Client-Side DOM Changes”SSR's job ends once result.html is sent — it enriches one server-rendered snapshot and does not know what happens in the browser afterward.
For .luis selectors marked dynamic/observed, the browser runtime keeps watching the DOM after hydration and injects their CSS automatically when a match appears (see the hybrid Browser Runtime docs).
For selectors marked initial (observed only against the SSR/build-time snapshot), the picture is different: a selector that did not match at SSR time is only re-checked when your app explicitly signals a DOM change — it is not watched automatically. Two ways to get that signal to fire:
- If you bootstrap with
createHybridLifecycleAdapter()/createHybridFrameworkEntryAdapter()and wiresubscribeDomCommitto your framework's own commit lifecycle (e.g. a ReactuseEffect, a VueonUpdated), this happens for you automatically after every commit. - Otherwise, call
notifyDomBatch()yourself after any client-side update that can add or remove DOM shaped like aninitial-mode selector (e.g. a badge that only appears once data loads).
About the Adapter Contract
Section titled “About the Adapter Contract”LentySsrAdapterContract (createLentySsrFrameworkAdapter()) is an optional seam, not a per-framework requirement. Use it only when you want the package to own the resolveRouteId -> renderHtml -> resolveSources -> finalizeHtml orchestration — for example to re-run finalizeHtml with the current request context on prod-cache hits. For everything else, the config-driven renderRoute() surface above is the recommended path.