Skip to content

SSR Frameworks

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.


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()

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)
}
})

src/middleware.ts
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.config.ts plugin snippet
import { 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
},
},
}
}

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:

Terminal window
npx lenty-ssr

See 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 wire subscribeDomCommit to your framework's own commit lifecycle (e.g. a React useEffect, a Vue onUpdated), 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 an initial-mode selector (e.g. a badge that only appears once data loads).

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.