ACF, Editable Schemas, and Exported Blocks
Share field definitions between page ACF, block attributes, and exported PHP without losing the source of truth.
ForgeWP uses two related but different kinds of WordPress content:
- Page or post fields such as ACF values, read by a template or section with
useWpMeta(). - Block attributes stored with the block in post content, edited through Gutenberg controls.
Use an editable schema to connect fields shared by page templates, ACF, and block attributes. The schema provides a common definition for those integrations.
If you are starting from the page side, scaffold the schema first:
pnpm forgewp make:editable AboutPage --fields=hero_title:text,hero_subtitle:richTextThis creates cms/editables/about-page.ts with editable and defaults exports. The command creates a field contract; it does not create a Gutenberg block. Use Page Content Slots and Editable Fields for the page-template workflow, then return here when a block should share those fields.
Define one editable schema
Put a page schema in cms/editables/. The schema is the source of field types and defaults used by ForgeWP's ACF/page-meta generation and by components that read those fields.
1import {
2 defineEditable,
3 getEditableDefaults,
4 richText,
5 text,
6} from "@forgewp/react"
7
8export const editable = defineEditable({
9 heading: text({
10 label: "Heading",
11 default: "Build the next section",
12 }),
13 body: richText({
14 label: "Body",
15 default: "A paragraph managed from WordPress.",
16 }),
17})
18
19export const defaults = getEditableDefaults(editable)The available field helpers include text, richText, image, boolean, color, url, select, number, icon, and repeater. A repeater can be fixed or dynamic; dynamic repeaters expose add/remove/reorder controls in the block sidebar.
Read a page field in a normal theme component with useWpMeta():
1import { useWpMeta } from "@forgewp/react"
2import { defaults } from "../../cms/editables/front-page"
3
4export function FrontPageHero() {
5 const heading = useWpMeta("heading", defaults.heading)
6 return <h1>{heading}</h1>
7}In local development, the hook reads the mock post context. In the exported theme, ForgeWP resolves the value from WordPress metadata/ACF. Empty values fall back to the supplied default.
Related hooks and primitives
These APIs solve different parts of the content workflow:
API Use it for useWpMeta(key, defaultValue)Read a structured editable field from block attributes first, then page/post metadata, then the default. useWpCustomField(key, defaultValue)Read a general custom field or ACF value when you are not sharing a defineEditable schema with a block.useIsEditorPreview()Add optional editor-only chrome when the component needs to know whether the generated block is being previewed in Gutenberg. It does not make content editable. useWpContent()Read the current post body as an HTML string when you need the value itself. <BlockArea />Render the current post's Gutenberg content in a bounded template slot. <WpEditable />Turn a selected string value into inline editing in local preview and RichText in Gutenberg.pickEditable() / mergeEditable()Build the block's attribute schema from shared page fields and block-only fields. |
|---|
Call these hooks at the top level of a component, following the normal Rules of Hooks. None of them is compulsory for every block. A block with only fixed JSX needs none; a block with inline text normally uses attributes, setAttributes, and WpEditable; a block sharing ACF fields additionally uses an editable schema and usually useWpMeta for the fixed-template host.
Share page fields with a block
A block can borrow fields from a page schema instead of declaring the same labels, types, and defaults a second time:
1import { pickEditable, WpEditable } from "@forgewp/react"
2import { editable as frontPageEditable } from "../../cms/editables/front-page"
3
4export const editable = pickEditable(frontPageEditable, {
5 title: "heading",
6 copy: "body",
7})
8
9/**
10 * @forgewp-block
11 * title: Featured Section
12 * category: theme
13 * icon: star-filled
14 */
15export function FeaturedSection({ title, copy, setAttributes }) {
16 return (
17 <section>
18 {setAttributes ? (
19 <WpEditable
20 tagName="h2"
21 value={title}
22 onChange={(value) => setAttributes({ title: value })}
23 />
24 ) : (
25 <h2>{title}</h2>
26 )}
27 <p>{copy}</p>
28 </section>
29 )
30}Here title and copy are block attribute names, while heading and body are the page/ACF keys. The block gets attributes from the schema, and WpEditable decides which of those attributes are inline-editable.
Use mergeEditable() when a block combines page content with block-only layout fields:
1import { mergeEditable, pickEditable, select } from "@forgewp/react"
2
3export const editable = mergeEditable(
4 pickEditable(frontPageEditable, { title: "heading" }),
5 {
6 paddingY: select({
7 label: "Vertical padding",
8 default: "md",
9 options: ["sm", "md", "lg"],
10 }),
11 }
12)Keep layout-only fields in the block schema. Do not add them to the page ACF group merely because the block uses them.
What happens when a block is saved?
For blocks that borrow page fields with pickEditable, ForgeWP records the attribute-to-meta mapping in generated block metadata and generated PHP. On post save, the generated synchronization hook walks the saved block tree and copies changed block attributes to ACF/post meta. This is the dual-host path:
The sync does not mean every block automatically updates ACF. It depends on a resolved pickEditable mapping. Untouched or omitted block attributes are not used to wipe existing meta. Test the actual saved post and the visitor page when using this pattern.
What export produces
A normal pnpm export compiles the block source into a WordPress theme. Depending on the path, inspect output similar to:
blocks/
└── featured-section/
├── block.json
└── render.php
assets/
└── forgewp-editor.js
functions.phpblock.jsoncontains the registered block name, attributes, supports, editor stylesheet, andrender.phpreference.render.phpis the visitor-facing PHP render generated from supported JSX and expressions.assets/forgewp-editor.jsregisters the compiled editor behavior and controls.functions.phpregisters the generated blocks oninitand contains the generated block/ACF synchronization hook when dual-host mappings exist.
Generated files are build output. Change the TSX component or schema, then export again; do not edit render.php, block.json, or generated wrappers as the source of truth.
JSDoc schema selection
For an annotated component that does not want a TypeScript import, the scanner also supports a from page schema and a static pick map in the same @forgewp-block comment:
1/**
2 * @forgewp-block
3 * title: Featured Section
4 * from: front-page
5 * pick:
6 * heading as title, body as copy
7 */
8export function FeaturedSection({ title, copy, setAttributes }) {
9 return <section>{title}</section>
10}The schema filename is resolved as cms/editables/front-page.ts. Keep the map static and export with warnings visible; use pnpm forgewp export --strict when you want unresolved schema mappings to fail the export.
Choose the field's owner.
Use useWpMeta() for page fields and attributes for values saved with an individual block. For shared values, choose defineEditable, pickEditable, and WpEditable according to where editors should save and change the content.