Each block lives in libs/blocks/src/lib/<name> and is made of three files: the Angular component, its template, and a block.ts holding the metadata the site and the CLI read. Names are conventionally suffixed with a number — login-01 , login-02 — so variations of the same idea group naturally.
Run the Generator
The generator creates the three files, exports them from libs/blocks/src/index.ts and registers the block in BLOCKS_REGISTRY . It finishes by printing the manual steps that remain.
npm run generate:block
# or, non-interactively:
npx nx generate @zardui/generators:block \
--name=login-06 \
--description="Split login screen with a testimonial panel" \
--category=login \
--label=Login--category is the registry bucket and is validated against the union below; --label is the display label stored in Block.category , and --title overrides the card heading. Passing a category that does not exist fails immediately instead of silently registering nothing.
Block Metadata
block.ts exports a single object typed as Block , imported from @doc/domain/components/block-container/block-container.component .
import type { Block } from '@doc/domain/components/block-container/block-container.component';
import { Login01Component } from './login-01.component';
export const login01Block: Block = {
id: 'login-01',
title: 'Login form',
description: 'A simple login form.',
component: Login01Component,
category: 'Login',
image: {
light: '/blocks/login-01/light.png',
dark: '/blocks/login-01/dark.png',
},
// Generated by `npm run sync:blocks` — do not edit by hand.
files: [],
};| Field | Type | Description |
|---|---|---|
id | string | The folder name. Used in the preview route /blocks/preview/:id. |
title | string | The heading shown on the block card. |
description | string | One sentence describing what the screen does. |
component | Type | The Angular component rendered in the live preview. |
category | string | A display label such as 'Authentication'. It is not the registry key. |
image | { light, dark } | Paths to the two screenshots served from apps/web/public/blocks/<id>/. |
files | BlockFile[] | The copyable sources. Generated by npm run sync:blocks — never edit by hand. |
Registry & Categories
BLOCKS_REGISTRY is a record keyed by BlockCategory , declared in domain/services/blocks.service.ts . These keys are fixed:
featuredsidebarloginsignupotpcalendarimport { login01Block, signup01Block } from '@blocks';
import type { BlockCategory } from '../services/blocks.service';
export const BLOCKS_REGISTRY: Record<BlockCategory, any[]> = {
featured: [login01Block, signup01Block],
sidebar: [],
login: [login01Block],
signup: [signup01Block],
otp: [],
calendar: [],
};Why blocks appear twice
The /blocks page renders one category at a time and defaults to featured . A block registered only under its own category would never show on the landing page, so the generator adds it to featured as well — exactly how login-01 is registered today.
What You Must Add
The generator stops at the scaffold. These four steps are yours.
Compose existing Zard components in <name>.component.ts and its template. A block should not introduce new primitives — if you need one, add it to libs/zard first.
Run npm run sync:blocks. It rewrites files[] from every .ts and .html in the folder except block.ts, so the code tab always matches the running preview.
Save light.png and dark.png under apps/web/public/blocks/<name>/. The generator prints the exact paths; without them the card renders a broken image.
Run npm run build:registry so zard-cli and the MCP server can install the new block.
# 1. Populate files[] from the component sources
npm run sync:blocks
# 2. Add the two screenshots the card renders
# apps/web/public/blocks/<name>/light.png
# apps/web/public/blocks/<name>/dark.png
# 3. Rebuild the registry consumed by the CLI
npm run build:registryThe screenshots are not optional
Nothing generates light.png and dark.png for you. Take them yourself at a desktop viewport, in both themes, and commit them alongside the block.