npm install --save-dev @gravity-ui/app-layoutWith express:
import express from 'express';
import {createRenderFunction} from '@gravity-ui/app-layout';
const app = express();
const renderLayout = createRenderFunction();
app.get('/', function (req, res) {
res.send(
renderLayout({
// RenderParams
title: 'Home page',
bodyContent: {
root: 'Hello world!',
},
}),
);
});
app.listen(3000);where
interface RenderParams<Data, Plugins> {
// Any json compatible data, will be set to window.__DATA__ on the page
data?: Data;
// favicon
icon?: Icon;
// nonce to be set on the appropriate tags
nonce?: string;
// base tag attributes
base?: Base;
// common options
// Page title
title: string;
// language of page, will be set to html tag
lang?: string;
isMobile?: boolean;
// html attributes
htmlAttributes?: string;
// header tag content
// meta tags
meta?: Meta[];
// link tags
links?: Link[];
// script tags
scripts?: Script[];
// style tags
styleSheets?: Stylesheet[];
// script tags with inlined code
inlineScripts?: string[];
// style tags with inlined styles
inlineStyleSheets?: string[];
// content of body tag
bodyContent?: {
// class name for body tag
className?: string;
// body attributes
attributes?: string;
// body content before div tag with id root
beforeRoot?: string;
// innerHtml content of div tag with id root
root?: string;
// body content after div tag with id root
afterRoot?: string;
};
// plugins options
pluginsOptions?: Partial<PluginsOptions<Plugins>>;
}Describes base tag:
interface Base {
href?: string;
target?: HTMLBaseElement['target'];
}Example:
renderLayout({
title: 'Home page',
base: {target: '_top'},
});Will be rendered as:
<base target="_top" />Describes meta tag:
interface Meta {
name: string;
content: string;
}Example:
const meta = [
{name: 'description', content: 'some text'},
{name: 'robots', content: 'noindex'},
{name: 'og:title', content: 'Some title'},
];Will be rendered as:
<meta name="description" content="some text" />
<meta name="robots" content="noindex" />
<meta property="og:title" content="Some title" />Describes page favicon:
interface Icon {
type?: string;
sizes?: string;
href?: string;
}Default value is:
const icon = {
type: 'image/png',
sizes: '16x16',
href: '/favicon.png',
};Describes link tag:
interface Link {
as?: string;
href: string;
rel?: string;
type?: string;
sizes?: string;
title?: HTMLLinkElement['title'];
crossOrigin?: '' | 'anonymous' | 'use-credentials';
}Example:
const link = {
href: 'myFont.woff2',
rel: 'preload',
as: 'font',
type: 'font/woff2',
crossOrigin: 'anonymous',
};will be rendered as:
<link href="myFont.woff2" rel="preload" as="font" type="font/woff2" crossorigin="anonymous" />Describes link to script with preload:
interface Script {
src: string;
defer?: boolean;
async?: boolean;
crossOrigin?: '' | 'anonymous' | 'use-credentials';
type?: 'importmap' | 'module' | string;
}Example:
const script = {
src: 'url/to/script',
defer: true,
async: false,
crossOrigin: 'anonymous',
};will be rendered as:
<link href="url/to/script" rel="preload" as="script" crossorigin="anonymous" />
<script src="url/to/script" defer="true" async="false" crossorigin="anonymous" nonce="..."></script>Describe link to styles:
interface Stylesheet {
href: string;
}Example:
const styleSheet = {
href: 'url/to/stylesheet',
};will be rendered as:
<link href="url/to/stylesheet" rel="stylesheet" />Render function can be extended by plugins. Plugin may rewrite user defined render content.
Plugin is an object with name and apply properties:
interface Plugin<Options = any, Name = string> {
name: Name;
apply: (params: {
options: Options | undefined; // passed through `renderLayout` function in `pluginsOptions` parameter.
commonOptions: CommonOptions;
renderContent: RenderContent;
/** @deprecated use `renderContent.helpers` instead */
utils: RenderHelpers;
}) => void;
}
interface CommonOptions {
name: string;
title: string;
lang?: string;
isMobile?: boolean;
}
export interface HeadContent {
base?: Base;
scripts: Script[];
helpers: RenderHelpers;
links: Link[];
meta: Meta[];
styleSheets: Stylesheet[];
inlineStyleSheets: string[];
inlineScripts: string[];
title: string;
}
export interface BodyContent {
attributes: Attributes;
beforeRoot: string[];
root?: string;
afterRoot: string[];
}
export interface RenderContent extends HeadContent {
htmlAttributes: Attributes;
bodyContent: BodyContent;
}
export interface RenderHelpers {
renderScript(script: Script): string;
renderInlineScript(content: string): string;
renderStyle(style: Stylesheet): string;
renderInlineStyle(content: string): string;
renderMeta(meta: Meta): string;
renderLink(link: Link): string;
attrs(obj: Attributes): string;
}There are some plugins in this package:
Adds google analytics counter on the page.
Usage:
import {createRenderFunction, createGoogleAnalyticsPlugin} from '@gravity-ui/app-layout';
const renderLayout = createRenderFunction([createGoogleAnalyticsPlugin()]);
app.get((req, res) => {
res.send(
renderLayout({
title: 'Home page',
pluginsOptions: {
googleAnalytics: {
useBeaconTransport: true, // enables use of navigator.sendBeacon
counter: {
id: 'some id',
},
},
},
}),
);
});Plugin options:
interface GoogleAnalyticsCounter {
id: string;
}
interface GoogleAnalyticsOptions {
useBeaconTransport?: boolean;
counter: GoogleAnalyticsCounter;
}Adds Yandex metrics counters on the page.
Usage:
import {createRenderFunction, createYandexMetrikaPlugin} from '@gravity-ui/app-layout';
const renderLayout = createRenderFunction([createYandexMetrikaPlugin()]);
app.get((req, res) => {
res.send(
renderLayout({
title: 'Home page',
pluginsOptions: {
yandexMetrika: {
counter: {
id: 123123123,
defer: true,
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
},
},
},
}),
);
});Plugin options:
export type UserParams = {
[x: string]: boolean | string | number | null | UserParams;
};
export interface MetrikaCounter {
id: number;
defer: boolean;
clickmap: boolean;
trackLinks: boolean;
accurateTrackBounce: boolean | number;
webvisor?: boolean;
nonce?: string;
encryptedExperiments?: string;
triggerEvent?: boolean;
trackHash?: boolean;
ecommerce?: boolean | string;
type?: number;
userParams?: UserParams;
}
export type MetrikaOptions = {
src?: string;
counter: MetrikaCounter | MetrikaCounter[];
};Adds script and styles from webpack assets manifest file.
Usage:
import {createRenderFunction, createLayoutPlugin} from '@gravity-ui/app-layout';
const renderLayout = createRenderFunction([
createLayoutPlugin({manifest: 'path/to/assets-manifest.json', publicPath: '/build/'}),
]);
app.get((req, res) => {
res.send(
renderLayout({
title: 'Home page',
pluginsOptions: {
layout: {
name: 'home',
},
},
}),
);
});Plugin options:
export interface LayoutOptions {
name: string;
prefix?: string;
}Loads initial scripts and stylesheets from backup addresses if loading from publicPath fails.
Usage:
createLayoutPlugin({
manifest: 'path/to/assets-manifest.json',
publicPath: 'https://static.example.com/build/',
publicPathFallbacks: ['https://backup.example.com/build/', '/build/'],
});These options are also available in createDefaultPlugins({layout: …}). Enable the layout plugin with pluginsOptions.layout.name, as in the example above. Omitting publicPathFallbacks or passing [] preserves normal rendering.
You can also use a callback function:
createLayoutPlugin({
manifest: 'path/to/assets-manifest.json',
publicPath: 'https://static.example.com/build/',
publicPathFallbacks: () =>
location.hostname.endsWith('.example.org')
? ['https://backup.example.org/build/', '/build/']
: ['/build/'],
});The function runs once and must be self-contained, without closures or imports. It receives {publicPath}; for root-relative primary paths, this value includes layout.prefix. Returning [] uses the primary address without retries.
The loader is bundled and minified when app-layout is built. The user callback is serialized from compiled server code using Function.prototype.toString(): transformations such as esbuild keepNames can inject external helpers (__name) that are unavailable in the browser. Test callbacks in your final server build. If bootstrap throws, synchronous registrations during HTML parsing restore the original tags via document.write, subject to browser restrictions; recovery cannot help if CSP blocks the registration scripts.
Retry options:
interface AssetRetryOptions {
retries?: number; // Additional attempts per address. Default: 0.
delayMs?: number; // Delay before the first retry, in milliseconds. Default: 500.
factor?: number; // Multiplier for subsequent retry delays. Default: 2.
maxDelayMs?: number; // Maximum retry delay, in milliseconds. Default: 3000.
timeoutMs?: number; // Attempt timeout, in milliseconds. Default: 10000; 0 disables.
}Pass these options as assetRetry to createLayoutPlugin. By default, a resource error switches to the next address without retrying the same URL. Set retries to enable additional attempts with exponential delays. The loader remembers the last successful address and starts subsequent resources there, cycling through the remaining addresses as backups. An individual failure does not change the preferred address; it changes only after a successful load. In-flight resources keep their candidate order. Explicit undefined option values use the defaults.
Each script or stylesheet attempt has a timeoutMs limit (10000 ms by default). A timeout follows the same retry policy as a loading error: retry the current URL while attempts remain, then try the next address. Set 0 to disable the timeout. The loader removes timed-out elements and ignores their late callbacks; removing a script does not guarantee cancellation of its execution.
Backup addresses must serve the same build files at the same relative paths. Do not include query parameters or fragments in base addresses. Trailing slashes are added and duplicate addresses are removed automatically. A path such as /build/ is relative to the page's origin; include any application prefix explicitly. Allow backup domains in CSP and configure CORS.
Loading behavior:
- Supports
deferscripts withoutasyncortype, and stylesheets under the primary path, preserving nonce andcrossOrigin. Other resources retain normal rendering. All managed assets of a response fragment are registered by a single inline script emitted right after the bootstrap, in head order (stylesheets, then scripts); their original tags are not rendered. - Stylesheets load in parallel. Scripts run in order after stylesheet attempts finish and the DOM is ready.
DOMContentLoadeddoes not mean the application has finished loading. - Final failures emit
app-layout:asset-load-erroronwindow. The event detail contains{type, originalUrl, attempts}and is described by the exportedAssetLoadErrortype;attemptslists attempted URLs, including retries. Register the handler before resources load. A final stylesheet loading failure does not block scripts; a final script loading failure stops the loader’s script queue. - For early scripts during streaming, use
renderScripts(content, scripts, state), thenrenderHeadContent(content, state)with the samestate: AssetRenderState = {}for one HTML response, even if content is regenerated. Bootstrap is emitted once; its initial configuration applies to the entire response. Directcontent.helperscalls render ordinary tags without fallback handling.
import {renderHeadContent, renderScripts, type AssetRenderState} from '@gravity-ui/app-layout';
const state: AssetRenderState = {};
res.write(renderScripts(content, firstScripts, state));
// ... await data and regenerate content if needed.
res.write(renderHeadContent(content, state));Lazy chunks require separate configuration, such as client.publicPathFallback in App Builder. Configure the backup addresses in both libraries; the fallback list is not shared through a global variable.
Adds body attributes.
Usage:
import {createRenderFunction, createUikitPlugin} from '@gravity-ui/app-layout';
const renderLayout = createRenderFunction([createUikitPlugin()]);
app.get((req, res) => {
res.send(
renderLayout({
title: 'Home page',
pluginsOptions: {
uikit: {
theme: 'dark',
direction: 'ltr',
},
},
}),
);
});Plugin options:
interface UikitPluginOptions {
theme: string;
direction?: 'ltr' | 'rtl';
}Adds microfrontend versions information to the page.
This plugin creates a global window.__REMOTE_VERSIONS__ object containing the provided microfrontend versions, which can be used by module federation or similar microfrontend architectures to determine which versions of remote modules to load.
It can be used in combination with App Builder and the moduleFederation.remotesRuntimeVersioning option to automatically load remote modules with the corresponding versions.
Usage:
import {createRenderFunction, createRemoteVersionsPlugin} from '@gravity-ui/app-layout';
const renderLayout = createRenderFunction([createRemoteVersionsPlugin()]);
app.get((req, res) => {
res.send(
renderLayout({
title: 'Home page',
pluginsOptions: {
remoteVersions: {
header: '1.2.3',
footer: '2.1.0',
sidebar: '0.5.1',
},
},
}),
);
});Plugin options:
type RemoteVersionsPluginOptions = Record<string, string>;There is helper to create all plugins:
import {createMiddleware, createDefaultPlugins} from '@gravity-ui/app-layout';
const renderLayout = createRenderFunction(
createDefaultPlugins({layout: {manifest: 'path/to/assets-manifest.json'}})
);
app.get((req, res) => {
res.send(renderLayout({
title: 'Home page',
pluginsOptions: {
layout: {
name: 'home'
},
googleAnalytics: {
counter: {...}
},
yandexMetrika: {
counter: {...}
},
},
}));
})With parts renderers generateRenderContent, renderHeadContent, renderBodyContent via html streaming:
import express from 'express';
import htmlescape from 'htmlescape';
import {
generateRenderContent,
renderHeadContent,
renderBodyContent,
createDefaultPlugins,
} from '@gravity-ui/app-layout';
const app = express();
app.get('/', async function (req, res) {
res.writeHead(200, {
'Content-Type': 'text/html',
'Transfer-Encoding': 'chunked',
});
const plugins = createDefaultPlugins({layout: {manifest: 'path/to/assets-manifest.json'}});
const content = generateRenderContent(plugins, {
title: 'Home page',
});
const {htmlAttributes, helpers, bodyContent} = content;
res.write(`
<!DOCTYPE html>
<html ${helpers.attrs({...htmlAttributes})}>
<head>
${renderHeadContent(content)}
</head>
<body ${helpers.attrs(bodyContent.attributes)}>
${renderBodyContent(content)}
`);
const data = await getUserData();
res.write(`
${content.renderHelpers.renderInlineScript(`
window.__DATA__ = ${htmlescape(data)};
`)}
</body>
</html>
`);
res.end();
});
app.listen(3000);