ページブロック:アクセシブルな 15 セクション(ヒーロー、料金、JSON-LD 付き FAQ など)、管理エディタ、拡張可能な登録
インストール
genpm add @core/blocks含まれるもの
- src/lib/blocks/ にソースコード(25 ファイル)。 (52.7 KB)
- src/lib/blocks/AGENTS.md に AI ルール、加えて IDE 用のルールファイル。
- @core/admin, @core/contracts, @core/db, @core/media, @core/rich-text, @core/ui を自動で解決します。
README
このパッケージには README がありません。
これは AI が src/lib/blocks で作業するときに読む内容そのものです。それ以外はコンテキストに追加されません。
@core/blocks — rules for AI agents
Purpose
Page sections that editors combine from the admin: hero, features, benefits, testimonials, logos, pricing, FAQ (with FAQPage JSON-LD), call to action, gallery, video, stats, comparison, guarantee, rich text and form. Each block is a zod schema + editor fields + a React component. Other packages and kits add their own blocks (product grid, latest posts…) to the same registry. Blocks are stored as JSON inside @core/content entries; no tables of its own.
Map
index.ts—defineBlock,createBlockRegistry,coreBlocks(and each block),RenderBlocks,collectMediaIds.core/*.ts— one file per block.parts.ts—Section,Heading,Img,CtaLink,JsonLdfor new blocks.media.ts—loadBlockMedia(blocks, registry)(server: one query to @core/media).editor.ts—'use client':blocksRenderer(blockMeta(registry))forAdminApp.blocks.css— styles on @core/ui tokens.
Integration
- Create
src/genpm/blocks.ts:export const blocks = createBlockRegistry([...coreBlocks /*, kit blocks */]); - Add a field to a content collection: schema
blocks: blocks.schema, admin fieldblocks.field('blocks', 'Sections'). - Admin: compute
blockMeta(blocks)on the server and pass it to the client component that renders<AdminApp renderers={{ blocks: blocksRenderer(meta), /* richText, media… */ }} />(never import the registry in client code). - Page (server component):
const media = await loadBlockMedia(page.data.blocks, blocks); return <RenderBlocks blocks={page.data.blocks} registry={blocks} media={media} renderForm={(key) => <Form formKey={key} />} embeds={hasConsent('marketing') ? 'iframe' : 'link'} />; - Styles: import
src/lib/ui/ui.cssthensrc/lib/blocks/blocks.cssin the root layout. - Verify: a page with every block passes axe and has one
h1; a block with an unknown type renders nothing and warns in dev.
Conventions
- A new block = new file with
defineBlock+ entry in the registry. Never addif (type === …)toRenderBlocks. - Use
SectionandHeadingwithctx.level(items one level below); only blocks withpageTitlemay become theh1. - Images are media ids resolved with
loadBlockMedia; links go through thehrefschema (nojavascript:). - Text in blocks is plain text; use the
rich-textblock for formatting.
Don't
- Don't render user HTML (
dangerouslySetInnerHTML) except viaJsonLd, which escapes<,>and&. - Don't load video iframes without marketing consent (
embeds: 'link'is the default). - Don't invent testimonials, ratings or statistics; the editor help texts remind users these must be real and verifiable.
# @core/blocks — rules for AI agents
## Purpose
Page sections that editors combine from the admin: hero, features, benefits, testimonials, logos, pricing, FAQ (with
FAQPage JSON-LD), call to action, gallery, video, stats, comparison, guarantee, rich text and form. Each block is a
zod schema + editor fields + a React component. Other packages and kits add their own blocks (product grid, latest
posts…) to the same registry. Blocks are stored as JSON inside @core/content entries; no tables of its own.
## Map
- `index.ts` — `defineBlock`, `createBlockRegistry`, `coreBlocks` (and each block), `RenderBlocks`, `collectMediaIds`.
- `core/*.ts` — one file per block. `parts.ts` — `Section`, `Heading`, `Img`, `CtaLink`, `JsonLd` for new blocks.
- `media.ts` — `loadBlockMedia(blocks, registry)` (server: one query to @core/media).
- `editor.ts` — `'use client'`: `blocksRenderer(blockMeta(registry))` for `AdminApp`. `blocks.css` — styles on @core/ui tokens.
## Integration
1. Create `src/genpm/blocks.ts`: `export const blocks = createBlockRegistry([...coreBlocks /*, kit blocks */]);`
2. Add a field to a content collection: schema `blocks: blocks.schema`, admin field `blocks.field('blocks', 'Sections')`.
3. Admin: compute `blockMeta(blocks)` on the server and pass it to the client component that renders
`<AdminApp renderers={{ blocks: blocksRenderer(meta), /* richText, media… */ }} />` (never import the registry in client code).
4. Page (server component):
```tsx
const media = await loadBlockMedia(page.data.blocks, blocks);
return <RenderBlocks blocks={page.data.blocks} registry={blocks} media={media}
renderForm={(key) => <Form formKey={key} />} embeds={hasConsent('marketing') ? 'iframe' : 'link'} />;
```
5. Styles: import `src/lib/ui/ui.css` then `src/lib/blocks/blocks.css` in the root layout.
6. Verify: a page with every block passes axe and has one `h1`; a block with an unknown type renders nothing and warns in dev.
## Conventions
- A new block = new file with `defineBlock` + entry in the registry. Never add `if (type === …)` to `RenderBlocks`.
- Use `Section` and `Heading` with `ctx.level` (items one level below); only blocks with `pageTitle` may become the `h1`.
- Images are media ids resolved with `loadBlockMedia`; links go through the `href` schema (no `javascript:`).
- Text in blocks is plain text; use the `rich-text` block for formatting.
## Don't
- Don't render user HTML (`dangerouslySetInnerHTML`) except via `JsonLd`, which escapes `<`, `>` and `&`.
- Don't load video iframes without marketing consent (`embeds: 'link'` is the default).
- Don't invent testimonials, ratings or statistics; the editor help texts remind users these must be real and verifiable.
.genpmignore 適用後に組み込まれる正確なツリーです。固定先:
// Definición y registro de bloques. Un bloque = tipo + esquema zod + campos del editor + componente React.
import type { ReactNode } from 'react';
import { z } from 'zod';
import type { AdminField } from '../contracts/index.ts';
import type { ResolvedImage } from '../rich-text/index.ts';
/** Bloque guardado en el contenido (`data` se valida con el esquema de su tipo). */
export type BlockInstance = { id: string; type: string; hidden?: boolean; data: Record<string, unknown> };
/** Lo que recibe cada componente además de sus datos. */
export type BlockContext = {
/** Id del bloque (anclas y `aria-labelledby`). */
id: string;
/** Nivel del encabezado principal del bloque (2 por defecto; 1 solo para el título de la página). */
level: 1 | 2 | 3 | 4 | 5 | 6;
/** Imágenes ya resueltas (`loadBlockMedia`). */
image: (id: unknown) => ResolvedImage | null;
/** Hueco para formularios de @core/forms u otros componentes que pone la app. */
renderForm?: (formKey: string) => ReactNode;
/** `iframe` solo con consentimiento; `link` por defecto (sin peticiones a terceros). */
embeds: 'link' | 'iframe';
siteHost?: string;
/** Primer bloque visible de la página. */
first: boolean;
};
export type BlockDefinition<S extends z.ZodType = z.ZodType> = {
type: string;
label: string;
/** Nombre de icono (lo interpreta el editor; opcional). */
icon?: string;
schema: S;
fields: AdminField[];
/** Datos de un bloque recién añadido en el editor. */
defaults?: () => Record<string, unknown>;
/** Este bloque puede ser el título de la página (`h1`): solo uno por página, lo decide `RenderBlocks`. */
pageTitle?: (data: z.output<S>) => boolean;
/** Puede ser async (Server Component) para bloques que leen datos: últimos posts, productos… */
Component: (props: { data: z.output<S>; ctx: BlockContext }) => ReactNode | Promise<ReactNode>;
};
/** Declara un bloque con inferencia de tipos de `data`. */
export const defineBlock = <S extends z.ZodType>(def: BlockDefinition<S>): BlockDefinition<S> => {
if (!/^[a-z][a-z0-9-]{0,39}$/.test(def.type)) throw new Error(`invalid block type: ${def.type}`);
return def;
};
export const BlockInstanceSchema = z.object({
id: z.string().min(1).max(40),
type: z.string().min(1).max(40),
hidden: z.boolean().optional(),
data: z.record(z.string(), z.unknown()),
});
export type BlockRegistry = {
get(type: string): BlockDefinition | undefined;
list(): BlockDefinition[];
/** Esquema zod para un campo de contenido con bloques (valida cada bloque con su tipo y aplica defaults). */
schema: z.ZodType<BlockInstance[], unknown>;
/** Campo `blocks` para el admin. */
field(name: string, label: string, opts?: Partial<AdminField>): AdminField;
};
/** Registro único de la app (`src/genpm/blocks.ts`): bloques base + los que añaden kits y paquetes. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- cada elemento tiene su propio tipo (recibe y devuelve su fila/datos): ningún tipo común es asignable en ambos sentidos
export function createBlockRegistry(defs: BlockDefinition<any>[], opts: { max?: number } = {}): BlockRegistry {
const map = new Map<string, BlockDefinition>();
for (const d of defs) {
if (map.has(d.type)) throw new Error(`duplicate block type: ${d.type}`);
map.set(d.type, d as BlockDefinition);
}
const schema = z
.array(BlockInstanceSchema)
.max(opts.max ?? 100)
.transform((blocks, ctx) => {
const ids = new Set<string>();
return blocks.map((b, i) => {
if (ids.has(b.id)) ctx.addIssue({ code: 'custom', path: [i, 'id'], message: `duplicate block id ${b.id}` });
ids.add(b.id);
const def = map.get(b.type);
if (!def) {
ctx.addIssue({ code: 'custom', path: [i, 'type'], message: `unknown block type ${b.type}` });
return b;
}
const r = def.schema.safeParse(b.data);
if (!r.success) {
for (const issue of r.error.issues) ctx.addIssue({ code: 'custom', path: [i, 'data', ...issue.path.map((p) => (typeof p === 'symbol' ? String(p) : p))], message: issue.message });
return b;
}
return { ...b, data: r.data as Record<string, unknown> };
});
}) as unknown as z.ZodType<BlockInstance[], unknown>;
return {
get: (t) => map.get(t),
list: () => [...map.values()],
schema,
field: (name, label, extra = {}) => ({ name, label, type: 'blocks', ...extra }),
};
}
/** Id corto para un bloque nuevo. */
export const newBlockId = () => Math.random().toString(36).slice(2, 10);
/** Ids de @core/media usados por los bloques (campos `media`, también dentro de listas). */
export function collectMediaIds(blocks: BlockInstance[], registry: BlockRegistry): string[] {
const out = new Set<string>();
const walk = (fields: AdminField[], data: unknown) => {
if (!data || typeof data !== 'object') return;
for (const f of fields) {
const v = (data as Record<string, unknown>)[f.name];
if (f.type === 'media') for (const id of Array.isArray(v) ? v : [v]) if (typeof id === 'string' && id) out.add(id);
if (f.type === 'list' && Array.isArray(v)) for (const item of v) walk(f.fields ?? [], item);
}
};
for (const b of blocks) {
const def = registry.get(b.type);
if (def && !b.hidden) walk(def.fields, b.data);
}
return [...out];
}
/** Texto plano visible de los bloques (búsqueda, extractos, llms.txt): campos de texto, listas y texto enriquecido. */
export function blocksToText(blocks: BlockInstance[] | null | undefined, registry: BlockRegistry, richTextToPlain: (doc: unknown) => string): string {
const out: string[] = [];
const walk = (fields: AdminField[], data: unknown) => {
if (!data || typeof data !== 'object') return;
for (const f of fields) {
const v = (data as Record<string, unknown>)[f.name];
if ((f.type === 'text' || f.type === 'textarea') && typeof v === 'string' && v.trim()) out.push(v.trim());
else if (f.type === 'richText' && v) out.push(richTextToPlain(v));
else if (f.type === 'list' && Array.isArray(v)) for (const item of v) walk(f.fields ?? [], item);
}
};
for (const b of blocks ?? []) {
const def = registry.get(b.type);
if (def && !b.hidden) walk(def.fields, b.data);
}
return out.filter(Boolean).join('\n');
}
/** Lo que el editor del panel necesita de cada bloque (serializable: se calcula en el servidor y viaja al cliente). */
export type BlockMeta = { type: string; label: string; icon?: string; fields: AdminField[]; defaults: Record<string, unknown> };
/** Metadatos de los bloques para `blocksRenderer` sin enviar al navegador componentes ni código de servidor. */
export function blockMeta(registry: BlockRegistry): BlockMeta[] {
return registry.list().map((d) => ({ type: d.type, label: d.label, ...(d.icon && { icon: d.icon }), fields: d.fields, defaults: d.defaults?.() ?? {} }));
}
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.0.0 | a255d6d | 3 時間前 | スキャン合格 |
- genpm
- @core/admin ^1.0.0@core/contracts ^1.0.0@core/db ^1.0.0@core/media ^1.0.0@core/rich-text ^1.0.0@core/ui ^1.0.0
- 提案
- GenPM は npm コマンドを提案し、あなたが承認した場合にのみ実行します。
- 利用元(1)
- @core/kit-cms ^1.0.0
- スキャン
- スキャン合格 · 指摘 0 件
- コミット
- v1.0.0 → a255d6d08201d6342d07ad41d96123ea1407a5f1 · 取得後に検証済み
- スクリプト
- なし。GenPM はパッケージのコードを実行しません。
- ライセンス
- MIT
- 品質
- 100/100
- 認識されたライセンス達成
- AGENTS.md に目的の説明がある達成
- AGENTS.md に統合手順がある達成
- AGENTS.md に規約や禁止事項がある達成
- テストを含む達成
- セキュリティスキャンに合格達成
- 過去 6 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?