This page explains the two separate config layers inside @lentystyle/ssr:
LentySsrConfigfor request-time rendering- the project-level
ssrblock insideluis.config.mjs
Minimal Setup
Section titled “Minimal Setup”pnpm add @lentystyle/ssrnpm install @lentystyle/ssryarn add @lentystyle/ssrTwo Separate Config Layers
Section titled “Two Separate Config Layers”LentySsrConfig is the public config type passed to renderLentySsrSnapshot(), createLentySsrIntegration(), and other request-time SSR surfaces.
This layer:
- defines runtime option defaults
- carries HTML injection metadata
- holds fields like
mode,adapter, andrewritePolicyin resolved form
LuisSsrProjectConfig is the ssr block inside luis.config.mjs.
This layer:
- holds shared app-level preview settings
- adds fields like
outDirName,include,exclude, andmaxEntries - is loaded via
loadLuisSsrProjectConfig()
LentySsrConfig
Section titled “LentySsrConfig”interface LentySsrConfig { mode?: 'static' | 'ssr' htmlGuardMode?: 'ssr' | 'hybrid' performance?: boolean debug?: boolean map?: boolean lazy?: boolean worker?: 'auto' | 'main' | 'worker' assetBaseUrl?: string globalCssHref?: string bootstrapScriptSrc?: string styleNonce?: string scriptNonce?: string runtimeOptions?: string | Partial<LentySsrRuntimeOptions> /** @deprecated Deprecated in 0.2.0 and ignored. */ adapter?: 'none' | 'vue' | 'svelte' | 'react' | 'astro' /** @deprecated Deprecated in 0.2.0 and ignored. */ rewritePolicy?: 'auto' | 'prefer-static' | 'prefer-runtime'}Defaults
Section titled “Defaults”| Field | Default |
|---|---|
mode | 'ssr' |
htmlGuardMode | 'ssr' (the project runner defaults to 'hybrid') |
debug | false |
performance | false |
map | false |
lazy | false |
worker | 'auto' |
assetBaseUrl | /_hybrid |
globalCssHref | null |
bootstrapScriptSrc | null |
styleNonce | null |
scriptNonce | null |
output.payloadMode | 'inline-json' (project config can opt into 'external-json'; see below) |
Quick Example
Section titled “Quick Example”import { createLentySsrIntegration } from '@lentystyle/ssr'
const ssr = createLentySsrIntegration({ mode: 'ssr', globalCssHref: '/assets/site.css', bootstrapScriptSrc: '/assets/browser-entry.js', runtimeOptions: 'performance,!worker',})In this example:
- the common global CSS is injected as
<link rel="stylesheet">on every render - the bootstrap script is injected on every render
- the runtime options resolution gives
performance: trueandworker: 'main'
CSP Nonces
Section titled “CSP Nonces”Two independent nonce fields target the two tags SSR can generate:
const result = await renderLentySsrSnapshot( { styleNonce: 'STYLE_NONCE', bootstrapScriptSrc: '/entry.js', scriptNonce: 'SCRIPT_NONCE' }, input,)styleNonceis written on the inline<style>tag only when CSS is non-emptyscriptNonceis written on the bootstrap<script src>tag only whenbootstrapScriptSrcis set- the inline JSON payload script is never executable, so it never receives a nonce
Both nonce values are reflected back on result.artifacts (styleNonce, scriptNonce) so you can confirm what was actually written, or null when the corresponding tag was not generated.
Payload Modes
Section titled “Payload Modes”type LentySsrPayloadMode = 'inline-json' | 'external-json'inline-json(default, and the only mode the request-time API resolves to): the route payload is embedded directly in the<script type="application/json">tag.external-json: only available through the project-levelssr.payloadModesetting, consumed by thelenty-ssrrunner. The runner writes<route-token>.payload.jsonunderassetBaseUrland the HTML gets a<script src="...">reference instead of an inline body.
export default defineLuisConfig({ ssr: { payloadMode: 'external-json' },})Built-in Guard Settings
Section titled “Built-in Guard Settings”SSR request helpers come with their own internal guard policy; you do not pass guardPolicy through config. The html scan mode follows htmlGuardMode and defaults to the strict 'ssr' allowlist.
| Surface | Guard behavior |
|---|---|
renderLentySsrSnapshot() | preset: 'strict', htmlMode: htmlGuardMode (default 'ssr') |
createLentySsrIntegration().renderRequest() | preset: 'strict', htmlMode: htmlGuardMode (default 'ssr') |
createLentySsrFrameworkAdapter().render() | preset: 'strict', htmlMode: htmlGuardMode (default 'ssr') |
Short example:
import { renderLentySsrSnapshot } from '@lentystyle/ssr'
await renderLentySsrSnapshot({}, { routeId: '/docs/', html: '<html><head></head><body><div class="card"></div></body></html>', sources: [ { sourceId: 'docs.luis', source: '.card { color: #0f172a; }', }, ],})Even if you do not provide extra guard config, this call validates the request envelope and maintains strict validation in the downstream hybrid compile step.
HTML Injection Point
Section titled “HTML Injection Point”Injection tries these locations in order:
<!-- lenty-ssr -->placeholder — if your html contains this comment, generated tags replace it (the comment itself disappears; only the first match is used)</head>— if no placeholder, tags are inserted right before the head close- prepend — if neither exists, tags are inserted at the very start of the html
<html> <head> <title>Docs</title> <!-- lenty-ssr --> </head> ...Production Cache Options
Section titled “Production Cache Options”createLentySsrProd() and createLentySsrProdFromProjectConfig() accept the same cache options:
interface LentySsrProdOptions { maxEntries?: number // default 100, applies to the default in-memory store ttlMs?: number // default undefined -> entries never expire on their own cacheStore?: LentySsrCacheStore<LentySsrProdRenderResult> // default: built-in in-memory store}The integration also exposes cache management methods:
await prod.invalidate(routeId) // forgets only that route's cached entriesawait prod.clear() // forgets everything (hit/miss counters are NOT reset)const stats = await prod.stats() // { hitCount, missCount, entryCount }Pluggable Cache Store
Section titled “Pluggable Cache Store”interface LentySsrCacheStore<TValue> { get: (key: string) => Promise<{ value: TValue; expiresAt: number | null } | undefined> set: (key: string, entry: { value: TValue; expiresAt: number | null }) => Promise<void> delete: (key: string) => Promise<void> clear: () => Promise<void> keys?: () => Promise<string[]> // optional; omit for stores that cannot enumerate keys}Implement this to back the cache with Redis or another shared store instead of the default process-local Map:
import { createLentySsrProd } from '@lentystyle/ssr'
const prod = createLentySsrProd({}, { ttlMs: 60_000, cacheStore: { async get(key) { /* ... */ }, async set(key, entry) { /* ... */ }, async delete(key) { /* ... */ }, async clear() { /* ... */ }, // no keys() -> eviction and exact entryCount are skipped for this store },}).luis Parse Cache
Section titled “.luis Parse Cache”This is a separate, smaller cache than the production result cache above — it only skips re-parsing .luis text, not the per-route CSS work.
import { createLentySsrCompileCache, createLentySsrProd, getLentySsrCompileCacheStats } from '@lentystyle/ssr'
const compileCache = createLentySsrCompileCache({ maxEntries: 500 }) // default 500const prod = createLentySsrProd({}, { compileCache })
getLentySsrCompileCacheStats(compileCache) // { entryCount, maxEntries }- keyed by
.luissource content hash, so it is always correct: two different routes sharing the same.luisfile produce exactly one cache entry - observed-rule matching against each route's html always runs fresh, every render — the cache never causes stale or missing CSS, it only skips re-parsing unchanged
.luistext createLentySsrProd(),createLentySsrProdIntegration(),createLentySsrFromProjectConfig(), andcreateLentySsrProdFromProjectConfig()all auto-create one whencompileCacheis omitted- pass the same instance to multiple integrations to share parse results between them
renderLentySsrSnapshot()and other one-off direct calls do not use a compile cache by default
Runtime Option Merge Order
Section titled “Runtime Option Merge Order”The runtime option merge order inside SSR is fixed:
package defaults-> config.debug / performance / map / lazy / worker-> config.runtimeOptions-> source.runtimeOptionsBase defaults are applied
{debug: false,performance: false,map: false,lazy: false,worker: 'auto',}Top-level config flags are applied
debug,performance,map,lazy, andworkerare applied at this stage.config.runtimeOptionspatch is appliedString or object patch overrides top-level flags.
Source-level override is applied
input.sources[].runtimeOptionsoverrides the config-level result per source.
String syntax
Section titled “String syntax”runtimeOptions: 'debug,performance,!worker'Result:
{ debug: true, performance: true, map: false, lazy: false, worker: 'main',}Supported tokens:
debug,performance,map,lazy!debug,!performance,!map,!lazyworker→worker: 'worker'!worker→worker: 'main'
Source-level override example
Section titled “Source-level override example”const result = await renderLentySsrSnapshot( { runtimeOptions: 'debug,worker' }, { routeId: '/docs/', html, sources: [ { sourceId: 'docs.luis', source, runtimeOptions: '!debug,lazy,!worker', }, ], },)Final runtime options for this source:
{ debug: false, performance: false, map: false, lazy: true, worker: 'main',}Request-level default and source-level override together
Section titled “Request-level default and source-level override together”Config:
const config = { runtimeOptions: 'debug,performance,worker',}Source:
const input = { routeId: '/docs/', html: '<html><head></head><body><div class="card"></div></body></html>', sources: [ { sourceId: 'docs.luis', source: '.card { color: #0f172a; }', runtimeOptions: '!debug,lazy,!worker', }, ],}Final runtime options for this source:
{ debug: false, performance: true, map: false, lazy: true, worker: 'main',}The ssr Block Inside luis.config.mjs
Section titled “The ssr Block Inside luis.config.mjs”The project-level SSR config type is LuisSsrProjectConfig. It inherits LentySsrConfig fields and adds preview or project-level fields.
import { defineLuisConfig } from '@lentystyle/core'
export default defineLuisConfig({ ssr: { mode: 'ssr', outDirName: 'dist-ssr', maxEntries: 100, defaultRuntimeOptions: '', include: ['docs/**', '**/*.html'], exclude: ['admin/**'], },})Additional project fields
Section titled “Additional project fields”| Field | Purpose | Default |
|---|---|---|
htmlDirName | Name of the built HTML input directory the runner reads | 'dist' |
outDirName | Name of the preview HTML root directory | 'dist-ssr' |
maxEntries | App-level cache limit for prod helper | 100 |
defaultRuntimeOptions | Default runtime option string merged into auto-discovered runner and configured route sources | '' |
include | Relative HTML path patterns to include | [] |
exclude | Relative HTML path patterns to skip | [] |
routes | Route-pattern to .luis source mappings for request-time renderRoute() | [] |
payloadMode | Payload delivery mode for runner-rendered routes ('inline-json' | 'external-json') | 'inline-json' |
routes entries take { match, sourceDirs?, sources? } where match is one route id pattern (or list) like '/docs/**', sourceDirs lists directories scanned for .luis files, and sources lists explicit files — both relative to the config directory. See the Frameworks page for full request-time recipes.
Project-level default example
Section titled “Project-level default example”import { defineLuisConfig } from '@lentystyle/core'
export default defineLuisConfig({ ssr: { mode: 'ssr', outDirName: 'dist-ssr', defaultRuntimeOptions: 'performance', include: ['docs/**'], },})In this setup defaultRuntimeOptions gives a common baseline to auto-discovered sources. If a source needs different behavior, override it with source-level runtimeOptions.
How to Load Project Config
Section titled “How to Load Project Config”loadLuisSsrProjectConfig(configOrPath?, overrides?)
Section titled “loadLuisSsrProjectConfig(configOrPath?, overrides?)”import { loadLuisSsrProjectConfig } from '@lentystyle/ssr'
const config = await loadLuisSsrProjectConfig(undefined, { cwd: appDir })This helper:
- loads the
luis.config.mjsfile - reads the
ssrblock - resolves request-level config fields
- applies project-level defaults like
outDirName,maxEntries,include, andexclude
Validation rules:
- throws if the config file does not export a real
ssrobject - if
includeandexcludeare provided, they must be string arrays outDirNamecannot be empty and cannot contain/or\
Normalized result
Section titled “Normalized result”{ configPath: '.../luis.config.mjs', configDir: '.../apps/site', outDirName: 'dist-ssr', outDir: '.../apps/site/dist-ssr', maxEntries: 100, defaultRuntimeOptions: '', include: [], exclude: [], runtimeOptions: { debug: false, performance: false, map: false, lazy: false, worker: 'auto', },}How include and exclude Work
Section titled “How include and exclude Work”shouldProcessLuisSsrProjectHtmlFile(htmlFilePath, config) converts the generated HTML path to a relative path under config.outDir and matches patterns.
import { loadLuisSsrProjectConfig, shouldProcessLuisSsrProjectHtmlFile,} from '@lentystyle/ssr'
const config = await loadLuisSsrProjectConfig(undefined, { cwd: appDir })
shouldProcessLuisSsrProjectHtmlFile( `${appDir}/dist-ssr/docs/index.html`, config,)Match rules:
- if
includeis empty, the file is included by default - if
includeis not empty, at least one pattern must match - then if
excludematches, the file is rejected
Example
Section titled “Example”ssr: { include: ['docs/**', 'blog/**'], exclude: ['blog/drafts/**'],}| Relative path | Decision |
|---|---|
docs/index.html | processed |
blog/post-1/index.html | processed |
blog/drafts/demo/index.html | skipped |
admin/index.html | skipped |
Supported mini-glob behavior:
***?
Path normalization always uses /.
Known Limits
Section titled “Known Limits”adapterandrewritePolicyare deprecated in 0.2.0 and ignored- request-time
LentySsrConfigalways resolvesoutput.payloadModeto'inline-json';'external-json'is only reachable through the project-levelssr.payloadModesetting (runner-only) - browser runtime support for fetching and booting from
external-jsonpayload files is not implemented yet defaultRuntimeOptionsis applied automatically by thelenty-ssrrunner and byrenderRoute()fromssr.routes; the low-level render API does not read it directlyincludeandexcludeonly decide based on generated HTML relative paths; source discovery is owned by the runnermaxEntrieseviction and exactstats().entryCountrequire the cache store to supportkeys(); custom stores without it (e.g. some remote caches) skip both