{"id":42325860,"url":"https://github.com/networkteam/zebra-utils","last_synced_at":"2026-01-27T13:09:46.927Z","repository":{"id":246359415,"uuid":"820918943","full_name":"networkteam/zebra-utils","owner":"networkteam","description":null,"archived":false,"fork":false,"pushed_at":"2025-07-04T12:25:49.000Z","size":221,"stargazers_count":1,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-29T00:48:08.158Z","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/networkteam.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-06-27T12:45:31.000Z","updated_at":"2025-07-04T12:25:52.000Z","dependencies_parsed_at":"2024-06-27T14:05:17.366Z","dependency_job_id":"1fad5d59-ed69-4afa-a8db-0082cae11d4b","html_url":"https://github.com/networkteam/zebra-utils","commit_stats":null,"previous_names":["networkteam/zebra-utils"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/networkteam/zebra-utils","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/networkteam%2Fzebra-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/networkteam%2Fzebra-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/networkteam%2Fzebra-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/networkteam%2Fzebra-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/networkteam","download_url":"https://codeload.github.com/networkteam/zebra-utils/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/networkteam%2Fzebra-utils/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28813274,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T12:25:15.069Z","status":"ssl_error","status_checked_at":"2026-01-27T12:25:05.297Z","response_time":168,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":"2026-01-27T13:09:46.405Z","updated_at":"2026-01-27T13:09:46.919Z","avatar_url":"https://github.com/networkteam.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @networkteam/zebra-utils\n\n`zebra-utils` is a collection of helper functions and utilities designed for projects using Zebra with the app router. This package is intended for internal use only.\n\n## Installation\n\nTo install `zebra-utils`, run:\n\n```bash\nyarn add @networkteam/zebra-utils\n// npm install @networkteam/zebra-utils\n```\n\n## Table of contents\n\n- [Zebra](#zebra)\n- [ImgProxy](#imgproxy)\n- [Styleguide](#styleguide)\n- [Utils](#utils)\n\n## Zebra\n\n### Revalidation\n\nThe `revalidate` function is used inside an API route to revalidate the document cache of Next.js. It compares the bearer token with a `REVALIDATE_TOKEN` environment variable.\n\n#### Environment variables\n\n```\nREVALIDATE_TOKEN=\n```\n\n#### API route\n\n```ts\n// app/api/revalidate.ts\nimport { revalidate } from '@networkteam/zebra-utils';\n\nexport const POST = revalidate;\n```\n\n### Internationalization\n\nFor multilanguage projects, it is necessary to create subfolders for each locale, e.g., `/en`, `/fr`. To avoid duplicated code, you can use these helpers to utilize the functions exported by page, layout and not-found inside the root-`[[...slug]]` directory, but with the locale prepended to the passed slug. By default, the prepended locale is `en`, but it can be overwritten, as shown in the example of the not-found page.\n\n#### Example Usage\n\npage file\n\n```ts\n// app/en/[[...slug]]/page.tsx\n\nimport { localizedMetadata, localizedPage } from '@networkteam/zebra-utils';\nimport Page, { generateMetadata as originalGenerateMetadata } from 'app/[[...slug]]/page';\n\nexport const generateMetadata = localizedMetadata(originalGenerateMetadata);\nexport default localizedPage(Page);\n```\n\nlayout file\n\n```ts\n// app/en/[[...slug]]/layout.tsx\n\nimport { localizedPage } from '@networkteam/zebra-utils';\nimport RootLayout from 'app/[[...slug]]/layout';\n\nexport default localizedPage(RootLayout);\n```\n\nnot-found file (with passed locale)\n\n```ts\n// app/fr/[[...slug]]/layout.tsx\n\nimport { localizedPage } from '@networkteam/zebra-utils';\nimport NotFound from 'app/[[...slug]]/not-found';\n\n// Pass the locale to localizedPage/localizedMetadata if other than 'en'\nexport default localizedPage(NotFound, 'fr');\n```\n\n#### Folder Structure\n\nFor the above approach to work, the folder structure needs to look like this. Note: there must be **no** other layout or not-found file in the root of the app directory:\n\n```\napp\n│\n└── [[...slug]]\n│   ├── layout.tsx\n│   ├── not-found.tsx\n│   └── page.tsx\n│\n└── en\n│   └── [[...slug]]\n│       ├── layout.tsx\n│       ├── not-found.tsx\n│       └── page.tsx\n│\n└── fr\n    └── [[...slug]]\n        ├── layout.tsx\n        ├── not-found.tsx\n        └── page.tsx\n```\n\n## ImgProxy\n\nThe next config provides a way to use a custom image loader for all (next)-images. It's possible to define custom imageSizes and deviceSizes as well, which are used to create the srcset of the responsive image. The image loader middleware restricts images with dimensions outside of these widths. It uses the default image sizes concatenated with the default device sizes used by Next.js.\n\n### Loader\n\nUnfortunately, it's not possible to pass a loader function directly to the next config; it has to be a file path. So, create an `imageLoade.ts` inside of the Next.js root directory (or somewhere else in the project), with the following content:\n\n```ts\n// imageLoader.ts\nimport { imgProxyLoader } from '@networkteam/zebra-utils';\n\nexport default imgProxyLoader('_image', ['s3://my-s3-url']);\n```\n\nThe `imgProxyLoader` creates a path with the provided width and quality (through the next image), as well as the base64 encoded src. The function takes a path segment (`_image` by default) which has to match the path segment in the following middleware. The second argument is an array of allowed source URLs. If the src of the image does not start with any of the provided URLs, the original src will be returned by the imgProxyLoader.\n\n### Middleware\n\nCreate a `middleware.ts` inside of the Next.js root directory with the following content:\n\n```ts\n// middleware.ts\nimport { imgProxyMiddleware } from '@networkteam/zebra-utils';\nimport type { NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\n\nexport const middleware = async (request: NextRequest) =\u003e {\n  if (request.nextUrl.pathname.startsWith('/_image/')) {\n    return imgProxyMiddleware(request);\n  }\n\n  return NextResponse.next();\n};\n\nexport const config = {\n  matcher: '/_image/:path*',\n};\n```\n\nThe middleware catches all paths starting with `/_image`, so all paths created by the imgproxy loader. Make sure the path segment matches the provided path segment of the imgproxy loader. Besides the request, `imgProxyMiddleware` takes `ImgProxyMiddlewareOptions`, where the allowedWidths could be overwritten.\n\n### Next config\n\nThe last part is to add the image loader to the `next.config.js`:\n\n```ts\n// next.config.js\nconst config = {\n  images: {\n    formats: ['image/avif', 'image/webp'],\n    loader: 'custom',\n    loaderFile: './imageLoader.ts',\n    // deviceSizes,\n    // imageSizes,\n  },\n};\n```\n\nIt's possible to define custom imageSizes/deviceSizes. Make sure to pass all sizes to the imgProxyMiddleware as well.\n\n## Styleguide\n\nThis package contains a styleguide which is easy to set up for all components of the project. To enable the styleguide, set the following environment variable to any truthy value:\n\n### Environment variables\n\n```\nENABLE_STYLEGUIDE=\"true\"\n```\n\n### Route\n\nFirst, create a folder structure for the styleguide in the app directory:\n\n```\napp\n│\n└── styleguide\n    └── [[...slug]]\n        ├── content.tsx\n        ├── layout.tsx\n        └── page.tsx\n\n```\n\nAssuming all styles and fonts are imported in the project's RootLayout, we can simply import the RootLayout into the styleguide's layout:\n\n```ts\nimport RootLayout from 'app/[[...slug]]/layout';\nimport { localizedPage } from '@networkteam/zebra-utils';\n\nexport default RootLayout;\n\n// With multiple languages use the localizedPage helper:\n// export default localizedPage(RootLayout);\n```\n\nIn the page.tsx export the Styleguide route provided by this package:\n\n```ts\nimport { Styleguide } from '@networkteam/zebra-utils';\nimport { content } from './content';\n\nexport default Styleguide(content);\n```\n\nIt's possible to use a different subpath for the styleguide, but make sure to pass the path to the `Styleguide` function as the second parameter, like `Styleguide(content, '/custom-path');`. It has to match the root folder name of the styleguide.\n\n### Content\n\nThe content.tsx contains the structure and all components for the styleguide:\n\n```ts\nimport { StyleguideColors, StyleguideContent } from '@networkteam/zebra-utils';\n\nimport Logo from 'lib/components/Logo';\nimport Signet from 'lib/components/Signet';\nimport TeaserCard from 'lib/components/TeaserCard';\n\nimport tailwindConfig from 'tailwind.config';\n\nexport const content: StyleguideContent = {\n  title: 'Styleguide',\n  description: 'My awesome styleguide',\n  pages: [\n    {\n      path: 'atoms',\n      title: 'Atoms',\n      description: 'Atoms are the smallest building blocks of a design system.',\n      pages: [\n        {\n          title: 'Logo',\n          variants: [\n            {\n              title: 'Default',\n              component: \u003cLogo /\u003e,\n            },\n            {\n              title: 'Signet',\n              component: \u003cSignet /\u003e,\n            },\n          ],\n        },\n        {\n          title: 'Colors',\n          variants: [\n            {\n              component: \u003cStyleguideColors tailwindConfig={tailwindConfig} groupShades /\u003e,\n            },\n          ],\n        },\n      ]\n    },\n    {\n      path: 'molecules',\n      title: 'Molecules',\n      description:\n        'Molecules are groups of atoms bonded together and are the smallest fundamental units of a compound.',\n      pages: [\n        {\n          title: 'Teaser Card',\n          variants: [\n            {\n              component: (\n                \u003cTeaserCard\n                  image=\"https://placebear.com/600/600\"\n                  headline=\"This is a Teaser\"\n                  text=\"Lorem ipsum dolor sit amet, consetetur sadipscing elitr...\"\n                  buttonLabel=\"read more\"\n                  href=\"#\"\n                /\u003e\n              ),\n            },\n          ],\n        },\n      ]\n    },\n    {\n      title: 'Organisms',\n      description:\n        'Organisms are relatively complex components composed of groups of molecules and/or atoms and/or other organisms.',\n      pages: []\n    }\n  ]\n```\n\nThis example follows the atomic design approach, but subpages could be named anything. The pages path is by default the page title (slugified), but could be explicitly set. Note that the Colors atom uses a custom function `StyleguideColors`, which extracts all colors of a passed `tailwindConfig` and displays them in a grid.\n\n## Utils\n\nThese are some helper functions which are often used in Zebra projects.\n\n### cn\n\nThis package provides a function named `cn`, based on the same-named function provided by `shadcn/ui`. It uses [clsx](https://github.com/lukeed/clsx) in combination with [tailwind-merge](https://github.com/gjtorikian/tailwind_merge). `tailwind-merge` merges Tailwind CSS classes to prevent style conflicts. Use it the same way as `classNames` or `clsx`:\n\n```ts\n\u003cdiv\n    className={cn('p-4 rounded-lg',\n        {\n            'bg-red-500': color === 'red',\n            'bg-blue-500': color === 'blue',\n        },\n        className\n    )}\n\u003e\n    {children}\n\u003c/div\u003e\n```\n\n### baseClasses\n\nThis function maps spacing properties of a provided node to Tailwind CSS classes. It takes a `NeosContentNode` and optionally a sizes map and returns a string containing Tailwind CSS classes for padding and margin, if appropriate properties are set inside the node.\n\n#### Usage\n\n```ts\n\u003cdiv className={baseClasses(node)}\u003e\u003c/div\u003e\n```\n\nThe margin property names are `marginXs`, `marginSm`, `marginMd`, `marginLg`, `marginXl`, and `marginXxl`. For padding, it's the same, but with `padding` as the prefix.\n\nThe sizes map is used to match property values to a Tailwind CSS spacing class, or at least the last part of it.\n\n#### defaultSizes\n\n```ts\nconst defaultSizes = new Map([\n  ['topExtraSmall', 't-12'],\n  ['topSmall', 't-20'],\n  ['topBase', 't-24'],\n  ['topLarge', 't-28'],\n  ['topExtraLarge', 't-36'],\n  ['extraSmall', 'y-12'],\n  ['small', 'y-20'],\n  ['base', 'y-24'],\n  ['large', 'y-28'],\n  ['extraLarge', 'y-36'],\n  ['bottomExtraSmall', 'b-12'],\n  ['bottomSmall', 'b-20'],\n  ['bottomBase', 'b-24'],\n  ['bottomLarge', 'b-28'],\n  ['bottomExtraLarge', 'b-36'],\n]);\n```\n\nWith the default sizes, when setting `marginMd` to `topLarge`, the resulting class would be `md:mt-28`. The sizes can be overwritten with the second argument of the `baseClasses` helper.\n\n### slugify\n\nThis is a simple helper to create URL safe strings, which can be used as id's.\n\n```ts\nslugify('This is a really cool article!'); // returns \"this-is-a-really-cool-article\"\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnetworkteam%2Fzebra-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnetworkteam%2Fzebra-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnetworkteam%2Fzebra-utils/lists"}