Defining a block
How to create a new block type
Block types are automatically registered when .tsx files are created in the blocks folder.
Most block files have two exports: a meta object for Gutenberg/editor metadata, and a default export created with defineBlock.
import { defineBlock } from "eddev/blocks"
export const meta: BlockMeta = {
title: "Hello World",
icon: "welcome-write-blog",
tags: ["#root"],
}
export default defineBlock("content/hello-world", (props) => {
return <div>Hello!</div>
})defineBlock
defineBlock returns the component you pass in, but its first argument matters for TypeScript. The name should match the file path under blocks/, without the extension.
For blocks/content/hello-world.tsx, use:
defineBlock("content/hello-world", (props) => {
return null
})That name connects the component to generated BlockProps, which are built from the paired GraphQL query in types.blocks.ts.
Block Meta
Exporting meta, typed with the global BlockMeta type, gives eddev enough information to register the block with ACF/Gutenberg and to expose lightweight metadata at runtime.
Everyday Fields
title: string{:ts}— The title of this block, shown in the editor.description?: string{:ts}— Short helper text shown in the editor.category?: "text" | "media" | "design" | "widgets" | "theme" | "embed" | "layouts"{:ts}— The inserter category.icon?: BlockIcon{:ts}— WordPress icon name for the inserter.keywords?: string[] | string{:ts}— Extra search terms for authors.tags?: string[]{:ts}— eddev placement tags such as#root,#inline, or a site-specific tag.flags?: Record<string, any>{:ts}— Small runtime hints used by wrappers, spacing, or editor styling.conditions?: object{:ts}— Limits availability by post type, template, parent, or ancestor.editMode?: "preview" | "edit" | "both"{:ts}— Controls whether ACF fields show as sidebar-only, edit-only, or toggleable.blockStyles?: { name: string; label: string; isDefault?: boolean }[]{:ts}— Gutenberg style variants for a block.
Conditions
Use conditions when a block should only be available in specific places:
export const meta: BlockMeta = {
title: "Card Grid",
icon: "grid-view",
tags: ["#subpage"],
conditions: {
postTypes: ["page"],
templates: ["default"],
},
}postTypes limits the block by WordPress post type. templates limits it by page template and implies the page post type. parents and ancestors are available for stricter nested-block constraints, but prefer InnerBlocks allowedBlocks for simple parent-child relationships.
Block Tags
Since different blocks have different contexts, and are sometimes specific to certain page templates, post types, or sections of a page — it can be helpful to give these contexts some kind of nickname. This is where tags come in. Note that this is not a WordPress thing, but rather something specific to the eddev stack.
The tagging system accepts any string, but our convention is to prefix authoring-context tags with #. You may still see older unprefixed tags in existing projects.
We typically use a couple of tags by default:
#inlinefor basic WYSIWYG blocks, like core headings/paragraphs/lists#rootfor blocks that can be added at a top level for a page
For a hypothetical content-heavy site, with homepages, landing pages, subpages, and a blog, you might have a set of tags like:
#homepage-root— blocks that can only be added to the homepage#landing-root— full-bleed blocks that can be added the root of a landing page#subpage-root— blocks that work within the subpage grid (eg, 9 cols on desktop, 12 cols on mobile)#article-root— blocks that can be used on an article page
We might also have additional tags for nested blocks, like a #button tag that can be attached to a 'Button Link' block, as well as an 'Add to Cart' button block.
These tags will be referenced:
- In the
allowedBlocksprop ofInnerBlocks - In the
rootBlocksparameter when configuring the editor for a template or post type
Block Flags
Flags (not to be confused with Tags) allow you to set arbitrary key/values, which can be consumed by other parts of your frontend/editor code.
You set flags by passing an object, with arbitrary key/values, like so:
export const meta: BlockMeta = {
title: "My Block",
flags: {
myFlag: true,
somethingElse: "green"
}
}Then, when using InnerBlocks or ContentBlocks, you can use the flags in wrapBlock and spacer props — or inside your editor config — to intelligently wrap, style or filter blocks based on these values, rather than by name.
Use cases for this system:
- Applying class names to the wrapper element in the editor
- Deciding which blocks to wrap in animation components on the frontend, and which to exclude.
- Deciding which blocks to wrap in layout classes in a page template
Keep flags small. They are copied into the block payload that React receives.
Post Meta Blocks
Most ACF fields attached to a block are saved against that block instance. postMetaBlock changes that relationship: the fields attached to the block are also registered against a WordPress post type.
This is useful for template-controlled blocks that represent the main metadata for a post, such as a film header, strand header, team member header, or product summary. It works best when the block is inserted by a template and hidden from the inserter, rather than added randomly by authors.
import { defineBlock, EditableText, usePostExcerptEditor, usePostTitleEditor } from "eddev/blocks"
export const meta: BlockMeta = {
title: "Program Strand",
icon: "tag",
inserter: false,
postMetaBlock: {
postTypes: ["strand"],
fieldName: "info",
},
}
export default defineBlock("strand/strand-header", (props) => {
return (
<header>
<EditableText as="h1" store={usePostTitleEditor(props.strand?.title!)} />
<EditableText as="p" store={usePostExcerptEditor(props.strand?.excerpt!)} />
{props.category?.name && <p>{props.category.name}</p>}
</header>
)
})query StrandHeader($postId: ID!) {
block {
strand_strand_header {
category {
name
}
}
}
strand(id: $postId, idType: DATABASE_ID) {
title
excerpt
}
}In this example, ACF fields assigned to strand/strand-header can be edited through the block, and are also made available on the strand post type under the info GraphQL field. eddev also limits the block to one instance per post and applies the configured post types.
For this pattern, usually combine:
postMetaBlockto attach fields to the post typeinserter: falseso authors do not add extra copies manually- an editor template,
headerTemplate, ortemplateso the block is always present
Advanced Fields
These are useful, but should not be the first thing you reach for:
inserter: false{:ts}hides blocks that are only inserted by templates or code.lazyLoad: false{:ts}disables dynamic loading for blocks that are small or almost always visible.frontendMode: "hidden" | "childrenOnly" | "default"{:ts}hides utility wrapper blocks or replaces them with their children on the frontend.templatePartmarks a block as a reusable template part such as a header, footer, or sidebar.cacheandtransformsexist for specialized cases. Check the currentBlockMetatype before using them.
Older projects may still use the legacy metadata format. New docs and new blocks should use export const meta.