{"id":24755162,"url":"https://github.com/gravity-ui/app-layout","last_synced_at":"2025-06-22T04:34:26.865Z","repository":{"id":62055481,"uuid":"534536590","full_name":"gravity-ui/app-layout","owner":"gravity-ui","description":null,"archived":false,"fork":false,"pushed_at":"2025-04-14T11:49:12.000Z","size":582,"stargazers_count":3,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-14T21:53:14.655Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gravity-ui.png","metadata":{"files":{"readme":"README-ru.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS","dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-09-09T07:06:23.000Z","updated_at":"2025-04-23T08:45:43.000Z","dependencies_parsed_at":"2023-01-28T13:16:52.583Z","dependency_job_id":"296ffddd-e317-4d1e-a6c4-72fa065ac8a5","html_url":"https://github.com/gravity-ui/app-layout","commit_stats":{"total_commits":22,"total_committers":5,"mean_commits":4.4,"dds":0.5,"last_synced_commit":"3eb8fd3d835fc7d830f4e997f9a6f205a705b63a"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":"gravity-ui/package-example","purl":"pkg:github/gravity-ui/app-layout","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fapp-layout","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fapp-layout/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fapp-layout/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fapp-layout/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gravity-ui","download_url":"https://codeload.github.com/gravity-ui/app-layout/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gravity-ui%2Fapp-layout/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259960572,"owners_count":22938091,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-28T12:36:40.165Z","updated_at":"2025-06-22T04:34:21.849Z","avatar_url":"https://github.com/gravity-ui.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @gravity-ui/app-layout \u0026middot; [![npm package](https://img.shields.io/npm/v/@gravity-ui/app-layout)](https://www.npmjs.com/package/@gravity-ui/app-layout) [![CI](https://img.shields.io/github/actions/workflow/status/gravity-ui/app-layout/.github/workflows/ci.yml?label=CI\u0026logo=github)](https://github.com/gravity-ui/app-layout/actions/workflows/ci.yml?query=branch:main)\n\n## Install\n\n```shell\nnpm install --save-dev @gravity-ui/app-layout\n```\n\n## Usage\n\nWith `express`:\n\n```js\nimport express from 'express';\nimport {createRenderFunction} from '@gravity-ui/app-layout';\n\nconst app = express();\n\nconst renderLayout = createRenderFunction();\n\napp.get('/', function (req, res) {\n  res.send(\n    renderLayout({\n      // RenderParams\n      title: 'Home page',\n      bodyContent: {\n        root: 'Hello world!',\n      },\n    }),\n  );\n});\n\napp.listen(3000);\n```\n\nwhere\n\n```typescript\ninterface RenderParams\u003cData, Plugins\u003e {\n  // Any json compatible data, will be set to window.__DATA__ on the page\n  data?: Data;\n  // favicon\n  icon?: Icon;\n  // nonce to be set on the appropriate tags\n  nonce?: string;\n\n  // common options\n  // Page title\n  title: string;\n  // language of page, will be set to html tag\n  lang?: string;\n  isMobile?: boolean;\n\n  // html attributes\n  htmlAttributes?: string;\n  // header tag content\n  // meta tags\n  meta?: Meta[];\n  // link tags\n  links?: Link[];\n  // script tags\n  scripts?: Script[];\n  // style tags\n  styleSheets?: Stylesheet[];\n  // script tags with inlined code\n  inlineScripts?: string[];\n  // style tags with inlined styles\n  inlineStyleSheets?: string[];\n\n  // content of body tag\n  bodyContent?: {\n    // class name for body tag\n    className?: string;\n    // body attributes\n    attributes?: string;\n    // body content before div tag with id root\n    beforeRoot?: string;\n    // innerHtml content of div tag with id root\n    root?: string;\n    // body content after div tag with id root\n    afterRoot?: string;\n  };\n  // plugins options\n  pluginsOptions?: Partial\u003cPluginsOptions\u003cPlugins\u003e\u003e;\n}\n```\n\n### Meta\n\nDescribes `meta` tag:\n\n```typescript\ninterface Meta {\n  name: string;\n  content: string;\n}\n```\n\nExample:\n\n```js\nconst meta = [\n  {name: 'description', content: 'some text'},\n  {name: 'robots', content: 'noindex'},\n  {name: 'og:title', content: 'Some title'},\n];\n```\n\nWill be rendered as:\n\n```html\n\u003cmeta name=\"description\" content=\"some text\" /\u003e\n\u003cmeta name=\"robots\" content=\"noindex\" /\u003e\n\u003cmeta property=\"og:title\" content=\"Some title\" /\u003e\n```\n\n### Icon\n\nDescribes page favicon:\n\n```typescript\ninterface Icon {\n  type?: string;\n  sizes?: string;\n  href?: string;\n}\n```\n\nDefault value is:\n\n```js\nconst icon = {\n  type: 'image/png',\n  sizes: '16x16',\n  href: '/favicon.png',\n};\n```\n\n### Links\n\nDescribes `link` tag:\n\n```typescript\ninterface Link {\n  as?: string;\n  href: string;\n  rel?: string;\n  type?: string;\n  sizes?: string;\n  title?: HTMLLinkElement['title'];\n  crossOrigin?: '' | 'anonymous' | 'use-credentials';\n}\n```\n\nExample:\n\n```js\nconst link = {\n  href: 'myFont.woff2',\n  rel: 'preload',\n  as: 'font',\n  type: 'font/woff2',\n  crossOrigin: 'anonymous',\n};\n```\n\nwill be rendered as:\n\n```html\n\u003clink href=\"myFont.woff2\" rel=\"preload\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\" /\u003e\n```\n\n### Scripts\n\nDescribes link to script with preload:\n\n```typescript\ninterface Script {\n  src: string;\n  defer?: boolean;\n  async?: boolean;\n  crossOrigin?: '' | 'anonymous' | 'use-credentials';\n  type?: 'importmap' | 'module' | string;\n}\n```\n\nExample:\n\n```js\nconst script = {\n  src: 'url/to/script',\n  defer: true,\n  async: false,\n  crossOrigin: 'anonymous',\n};\n```\n\nwill be rendered as:\n\n```html\n\u003clink href=\"url/to/script\" rel=\"preload\" as=\"script\" crossorigin=\"anonymous\" /\u003e\n\n\u003cscript src=\"url/to/script\" defer=\"true\" async=\"false\" crossorigin=\"anonymous\" nonce=\"...\"\u003e\u003c/script\u003e\n```\n\n#### Style sheets\n\nDescribe link to styles:\n\n```typescript\ninterface Stylesheet {\n  href: string;\n}\n```\n\nExample:\n\n```js\nconst styleSheet = {\n  href: 'url/to/stylesheet',\n};\n```\n\nwill be rendered as:\n\n```html\n\u003clink href=\"url/to/stylesheet\" rel=\"stylesheet\" /\u003e\n```\n\n## Plugins\n\nRender function can be extended by plugins. Plugin may rewrite user defined render content.\nPlugin is an object with `name` and `apply` properties:\n\n```typescript\ninterface Plugin\u003cOptions = any, Name = string\u003e {\n  name: Name;\n  apply: (params: {\n    options: Options | undefined; // passed through `renderLayout` function in `pluginsOptions` parameter.\n    commonOptions: CommonOptions;\n    renderContent: RenderContent;\n    /** @deprecated use `renderContent.helpers` instead */\n    utils: RenderHelpers;\n  }) =\u003e void;\n}\n\ninterface CommonOptions {\n  name: string;\n  title: string;\n  lang?: string;\n  isMobile?: boolean;\n}\n\nexport interface HeadContent {\n  scripts: Script[];\n  helpers: RenderHelpers;\n  links: Link[];\n  meta: Meta[];\n  styleSheets: Stylesheet[];\n  inlineStyleSheets: string[];\n  inlineScripts: string[];\n  title: string;\n}\n\nexport interface BodyContent {\n  attributes: Attributes;\n  beforeRoot: string[];\n  root?: string;\n  afterRoot: string[];\n}\n\nexport interface RenderContent extends HeadContent {\n  htmlAttributes: Attributes;\n  bodyContent: BodyContent;\n}\n\nexport interface RenderHelpers {\n  renderScript(script: Script): string;\n  renderInlineScript(content: string): string;\n  renderStyle(style: Stylesheet): string;\n  renderInlineStyle(content: string): string;\n  renderMeta(meta: Meta): string;\n  renderLink(link: Link): string;\n  attrs(obj: Attributes): string;\n}\n```\n\nThere are some plugins in this package:\n\n### Google analytics\n\nAdds google analytics counter on the page.\n\nUsage:\n\n```js\nimport {createRenderFunction, createGoogleAnalyticsPlugin} from '@gravity-ui/app-layout';\n\nconst renderLayout = createRenderFunction([createGoogleAnalyticsPlugin()]);\n\napp.get((req, res) =\u003e {\n  res.send(\n    renderLayout({\n      title: 'Home page',\n      pluginsOptions: {\n        googleAnalytics: {\n          useBeaconTransport: true, // enables use of navigator.sendBeacon\n          counter: {\n            id: 'some id',\n          },\n        },\n      },\n    }),\n  );\n});\n```\n\nPlugin options:\n\n```typescript\ninterface GoogleAnalyticsCounter {\n  id: string;\n}\n\ninterface GoogleAnalyticsOptions {\n  useBeaconTransport?: boolean;\n  counter: GoogleAnalyticsCounter;\n}\n```\n\n### Yandex Metrika\n\nAdds Yandex metrics counters on the page.\n\nUsage:\n\n```js\nimport {createRenderFunction, createYandexMetrikaPlugin} from '@gravity-ui/app-layout';\n\nconst renderLayout = createRenderFunction([createYandexMetrikaPlugin()]);\n\napp.get((req, res) =\u003e {\n  res.send(\n    renderLayout({\n      title: 'Home page',\n      pluginsOptions: {\n        yandexMetrika: {\n          counter: {\n            id: 123123123,\n            defer: true,\n            clickmap: true,\n            trackLinks: true,\n            accurateTrackBounce: true,\n          },\n        },\n      },\n    }),\n  );\n});\n```\n\nPlugin options:\n\n```typescript\nexport type UserParams = {\n  [x: string]: boolean | string | number | null | UserParams;\n};\n\nexport interface MetrikaCounter {\n  id: number;\n  defer: boolean;\n  clickmap: boolean;\n  trackLinks: boolean;\n  accurateTrackBounce: boolean | number;\n  webvisor?: boolean;\n  nonce?: string;\n  encryptedExperiments?: string;\n  triggerEvent?: boolean;\n  trackHash?: boolean;\n  ecommerce?: boolean | string;\n  type?: number;\n  userParams?: UserParams;\n}\n\nexport type MetrikaOptions = {\n  src?: string;\n  counter: MetrikaCounter | MetrikaCounter[];\n};\n```\n\n### Layout\n\nAdds script and styles from webpack assets manifest file.\n\nUsage:\n\n```js\nimport {createRenderFunction, createLayoutPlugin} from '@gravity-ui/app-layout';\n\nconst renderLayout = createRenderFunction([\n  createLayoutPlugin({manifest: 'path/to/assets-manifest.json', publicPath: '/build/'}),\n]);\n\napp.get((req, res) =\u003e {\n  res.send(\n    renderLayout({\n      title: 'Home page',\n      pluginsOptions: {\n        layout: {\n          name: 'home',\n        },\n      },\n    }),\n  );\n});\n```\n\nPlugin options:\n\n```typescript\nexport interface LayoutOptions {\n  name: string;\n  prefix?: string;\n}\n```\n\n### @gravity-ui/uikit\n\nAdds body attributes.\n\nUsage:\n\n```js\nimport {createRenderFunction, createUikitPlugin} from '@gravity-ui/app-layout';\n\nconst renderLayout = createRenderFunction([createUikitPlugin()]);\n\napp.get((req, res) =\u003e {\n  res.send(\n    renderLayout({\n      title: 'Home page',\n      pluginsOptions: {\n        uikit: {\n          theme: 'dark',\n          direction: 'ltr',\n        },\n      },\n    }),\n  );\n});\n```\n\nPlugin options:\n\n```typescript\ninterface UikitPluginOptions {\n  theme: string;\n  direction?: 'ltr' | 'rtl';\n}\n```\n\n### Helpers\n\nThere is helper to create all plugins:\n\n```js\nimport {createMiddleware, createDefaultPlugins} from '@gravity-ui/app-layout';\n\nconst renderLayout = createRenderFunction(\n    createDefaultPlugins({layout: {manifest: 'path/to/assets-manifest.json'}})\n);\n\napp.get((req, res) =\u003e {\n    res.send(renderLayout({\n        title: 'Home page',\n        pluginsOptions: {\n            layout: {\n                name: 'home'\n            },\n            googleAnalytics: {\n                counter: {...}\n            },\n            yandexMetrika: {\n                counter: {...}\n            },\n        },\n    }));\n})\n```\n\n## Alternative usage\n\nWith parts renderers `generateRenderContent`, `renderHeadContent`, `renderBodyContent` via html streaming:\n\n```js\nimport express from 'express';\nimport htmlescape from 'htmlescape';\nimport {\n  generateRenderContent,\n  renderHeadContent,\n  renderBodyContent,\n  createDefaultPlugins,\n} from '@gravity-ui/app-layout';\n\nconst app = express();\n\napp.get('/', async function (req, res) {\n  res.writeHead(200, {\n    'Content-Type': 'text/html',\n    'Transfer-Encoding': 'chunked',\n  });\n\n  const plugins = createDefaultPlugins({layout: {manifest: 'path/to/assets-manifest.json'}});\n\n  const content = generateRenderContent(plugins, {\n    title: 'Home page',\n  });\n\n  const {htmlAttributes, helpers, bodyContent} = content;\n\n  res.write(`\n        \u003c!DOCTYPE html\u003e\n        \u003chtml ${helpers.attrs({...htmlAttributes})}\u003e\n        \u003chead\u003e\n            ${renderHeadContent(content)}\n        \u003c/head\u003e\n        \u003cbody ${helpers.attrs(bodyContent.attributes)}\u003e\n            ${renderBodyContent(content)}\n    `);\n\n  const data = await getUserData();\n\n  res.write(`\n            ${content.renderHelpers.renderInlineScript(`\n                window.__DATA__ = ${htmlescape(data)};\n            `)}\n        \u003c/body\u003e\n        \u003c/html\u003e\n    `);\n  res.end();\n});\n\napp.listen(3000);\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgravity-ui%2Fapp-layout","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgravity-ui%2Fapp-layout","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgravity-ui%2Fapp-layout/lists"}