Skip to content

SSR Config

This page explains the two separate config layers inside @lentystyle/ssr:

  • LentySsrConfig for request-time rendering
  • the project-level ssr block inside luis.config.mjs

Terminal window
pnpm add @lentystyle/ssr

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, and rewritePolicy in resolved form

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'
}
FieldDefault
mode'ssr'
htmlGuardMode'ssr' (the project runner defaults to 'hybrid')
debugfalse
performancefalse
mapfalse
lazyfalse
worker'auto'
assetBaseUrl/_hybrid
globalCssHrefnull
bootstrapScriptSrcnull
styleNoncenull
scriptNoncenull
output.payloadMode'inline-json' (project config can opt into 'external-json'; see below)
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: true and worker: 'main'

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,
)
  • styleNonce is written on the inline <style> tag only when CSS is non-empty
  • scriptNonce is written on the bootstrap <script src> tag only when bootstrapScriptSrc is 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.


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-level ssr.payloadMode setting, consumed by the lenty-ssr runner. The runner writes <route-token>.payload.json under assetBaseUrl and the HTML gets a <script src="..."> reference instead of an inline body.
export default defineLuisConfig({
ssr: { payloadMode: 'external-json' },
})

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.

SurfaceGuard 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.


Injection tries these locations in order:

  1. <!-- lenty-ssr --> placeholder — if your html contains this comment, generated tags replace it (the comment itself disappears; only the first match is used)
  2. </head> — if no placeholder, tags are inserted right before the head close
  3. prepend — if neither exists, tags are inserted at the very start of the html
<html>
<head>
<title>Docs</title>
<!-- lenty-ssr -->
</head>
...

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 entries
await prod.clear() // forgets everything (hit/miss counters are NOT reset)
const stats = await prod.stats() // { hitCount, missCount, entryCount }
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
},
})

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 500
const prod = createLentySsrProd({}, { compileCache })
getLentySsrCompileCacheStats(compileCache) // { entryCount, maxEntries }
  • keyed by .luis source content hash, so it is always correct: two different routes sharing the same .luis file 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 .luis text
  • createLentySsrProd(), createLentySsrProdIntegration(), createLentySsrFromProjectConfig(), and createLentySsrProdFromProjectConfig() all auto-create one when compileCache is 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

The runtime option merge order inside SSR is fixed:

package defaults
-> config.debug / performance / map / lazy / worker
-> config.runtimeOptions
-> source.runtimeOptions
  1. Base defaults are applied

    {
    debug: false,
    performance: false,
    map: false,
    lazy: false,
    worker: 'auto',
    }
  2. Top-level config flags are applied

    debug, performance, map, lazy, and worker are applied at this stage.

  3. config.runtimeOptions patch is applied

    String or object patch overrides top-level flags.

  4. Source-level override is applied

    input.sources[].runtimeOptions overrides the config-level result per source.

runtimeOptions: 'debug,performance,!worker'

Result:

{
debug: true,
performance: true,
map: false,
lazy: false,
worker: 'main',
}

Supported tokens:

  • debug, performance, map, lazy
  • !debug, !performance, !map, !lazy
  • workerworker: 'worker'
  • !workerworker: 'main'
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 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/**'],
},
})
FieldPurposeDefault
htmlDirNameName of the built HTML input directory the runner reads'dist'
outDirNameName of the preview HTML root directory'dist-ssr'
maxEntriesApp-level cache limit for prod helper100
defaultRuntimeOptionsDefault runtime option string merged into auto-discovered runner and configured route sources''
includeRelative HTML path patterns to include[]
excludeRelative HTML path patterns to skip[]
routesRoute-pattern to .luis source mappings for request-time renderRoute()[]
payloadModePayload 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.

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.


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.mjs file
  • reads the ssr block
  • resolves request-level config fields
  • applies project-level defaults like outDirName, maxEntries, include, and exclude

Validation rules:

  • throws if the config file does not export a real ssr object
  • if include and exclude are provided, they must be string arrays
  • outDirName cannot be empty and cannot contain / or \
{
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',
},
}

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 include is empty, the file is included by default
  • if include is not empty, at least one pattern must match
  • then if exclude matches, the file is rejected
ssr: {
include: ['docs/**', 'blog/**'],
exclude: ['blog/drafts/**'],
}

Supported mini-glob behavior:

  • *
  • **
  • ?

Path normalization always uses /.


  • adapter and rewritePolicy are deprecated in 0.2.0 and ignored
  • request-time LentySsrConfig always resolves output.payloadMode to 'inline-json'; 'external-json' is only reachable through the project-level ssr.payloadMode setting (runner-only)
  • browser runtime support for fetching and booting from external-json payload files is not implemented yet
  • defaultRuntimeOptions is applied automatically by the lenty-ssr runner and by renderRoute() from ssr.routes; the low-level render API does not read it directly
  • include and exclude only decide based on generated HTML relative paths; source discovery is owned by the runner
  • maxEntries eviction and exact stats().entryCount require the cache store to support keys(); custom stores without it (e.g. some remote caches) skip both