npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gravity-ui/app-layout

v2.8.2

Published

## Install

Readme

@gravity-ui/app-layout · npm package CI

Install

npm install --save-dev @gravity-ui/app-layout

Usage

With 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>>;
}

Base

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" />

Meta

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" />

Icon

Describes page favicon:

interface Icon {
  type?: string;
  sizes?: string;
  href?: string;
}

Default value is:

const icon = {
  type: 'image/png',
  sizes: '16x16',
  href: '/favicon.png',
};

Links

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" />

Scripts

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>

Style sheets

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" />

Plugins

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:

Google analytics

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

Yandex Metrika

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[];
};

Layout

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

Public path fallbacks

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 script 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 scripts there, cycling through the remaining addresses as backups. Stylesheets always start at publicPath, then try backup addresses in the configured order. 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 defer scripts without async or type, and stylesheets under the primary path, preserving nonce and crossOrigin. 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); original script tags are not rendered.
  • Stylesheets are included in the HTML and load in parallel, even if the JavaScript loader cannot start. Partially styled content may be visible during retries. Scripts run in order after stylesheet attempts finish and the DOM is ready. DOMContentLoaded does not mean the application has finished loading.
  • Final failures emit app-layout:asset-load-error on window. The event detail contains {type, originalUrl, attempts} and is described by the exported AssetLoadError type; attempts lists 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), then renderHeadContent(content, state) with the same state: AssetRenderState = {} for one HTML response, even if content is regenerated. Call renderHeadContent once per HTML response. Bootstrap is emitted once; its initial configuration applies to the entire response. Direct content.helpers calls render ordinary tags without fallback handling.
  • During streaming, a stylesheet's timeoutMs starts when its reference arrives in the HTML. Registered stylesheet links that cannot be found by DOMContentLoaded are reported as failed and do not block scripts.
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.

@gravity-ui/uikit

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';
}

Remote Versions

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>;

Helpers

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: {...}
            },
        },
    }));
})

Alternative usage

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