ページブロック:アクセシブルな 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 適用後に組み込まれる正確なツリーです。固定先:
// Piezas comunes de los bloques: encabezados con nivel, imágenes resueltas, enlaces seguros y JSON-LD.
import { createElement as h, type ReactNode } from 'react';
import { z } from 'zod';
import { isSafeHref, type ResolvedImage } from '../rich-text/index.ts';
import type { BlockContext } from './define.ts';
type Level = BlockContext['level'];
const clamp = (n: number) => Math.min(Math.max(n, 1), 6) as Level;
export function Heading(p: { level: number; children?: ReactNode; className?: string; id?: string }): ReactNode {
return h(`h${clamp(p.level)}`, { className: p.className, id: p.id }, p.children);
}
export function Img(p: { image: ResolvedImage | null; className?: string; priority?: boolean; alt?: string }): ReactNode {
if (!p.image) return null;
const { src, alt, width, height, srcSet, sizes } = p.image;
return h('img', { src, alt: p.alt ?? alt, width, height, srcSet, sizes, className: p.className, loading: p.priority ? 'eager' : 'lazy', decoding: 'async', ...(p.priority && { fetchPriority: 'high' }) });
}
/** Enlace de llamada a la acción: solo rutas relativas, http(s), mailto y tel. */
export function CtaLink(p: { href?: string | null; label?: string | null; variant?: 'primary' | 'default'; siteHost?: string | undefined }): ReactNode {
if (!p.href || !p.label || !isSafeHref(p.href)) return null;
const external = /^https?:\/\//i.test(p.href) && (() => {
try {
return new URL(p.href).host !== p.siteHost;
} catch {
return true;
}
})();
return h('a', { href: p.href, className: `ui-button${p.variant === 'primary' ? ' ui-button--primary' : ''}`, ...(external && { rel: 'noopener noreferrer' }) }, p.label);
}
/** `<script type="application/ld+json">` sin posibilidad de cerrar la etiqueta desde los datos. */
export function JsonLd(p: { data: unknown }): ReactNode {
const json = JSON.stringify(p.data).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
return h('script', { type: 'application/ld+json', dangerouslySetInnerHTML: { __html: json } });
}
/** Sección estándar: `<section aria-labelledby>` con encabezado opcional e introducción. */
export function Section(p: { id: string; block: string; title?: string | undefined; intro?: string | undefined; ctx: BlockContext; children?: ReactNode; className?: string; label?: string }): ReactNode {
const hid = `${p.id}-title`;
return h(
'section',
{ className: `blk blk-${p.block}${p.className ? ` ${p.className}` : ''}`, ...(p.title ? { 'aria-labelledby': hid } : p.label ? { 'aria-label': p.label } : {}) },
p.title && h(Heading, { level: p.ctx.level, id: hid, className: 'blk__title' }, p.title),
p.intro && h('p', { className: 'blk__intro' }, p.intro),
p.children,
);
}
// Esquemas reutilizables.
export const text = (max = 200) => z.string().trim().min(1).max(max);
export const optText = (max = 200) => z.string().trim().max(max).optional().transform((v) => v || undefined);
export const href = z
.string()
.trim()
.max(500)
.refine((v) => !v || isSafeHref(v), 'Unsafe link')
.optional()
.transform((v) => v || undefined);
export const mediaId = z.string().max(40).optional().nullable().transform((v) => v || undefined);
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.0.0 | a255d6d | 7 時間前 | スキャン合格 |
- 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 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?