Composing UI with WordPress Data
Build reusable theme components by combining ForgeWP's WordPress-aware primitives with ordinary React markup.
A component registry gives you starting points for buttons, cards, navigation, and other interface pieces. ForgeWP's data primitives solve a different problem: they connect React markup to WordPress posts and menus.
You can combine them. Keep the visual structure in your own React components, then use ForgeWP's WordPress-aware components and hooks where the content comes from WordPress.
Build a post card
A WordPress post is made available to nested data hooks by <WpQueryLoop>. Put the card in its own component and call the hooks there. This keeps the card reusable and makes it clear which content each value comes from.
1import {
2 useWpExcerpt,
3 useWpFeaturedImage,
4 useWpPermalink,
5 useWpTitle,
6} from "@forgewp/react"
7
8export function PostCard() {
9 const title = useWpTitle()
10 const excerpt = useWpExcerpt()
11 const image = useWpFeaturedImage()
12 const permalink = useWpPermalink()
13
14 return (
15 <article className="border border-zinc-300">
16 <a href={permalink}>
17 <img
18 src={image}
19 alt={title}
20 className="aspect-video w-full object-cover"
21 />
22 <div className="p-4">
23 <h2 className="text-xl font-semibold">{title}</h2>
24 <p className="mt-2">{excerpt}</p>
25 </div>
26 </a>
27 </article>
28 )
29}Render one card for each post by placing it inside the loop:
1import { WpQueryLoop } from "@forgewp/react"
2import { PostCard } from "../components/PostCard"
3
4export default function HomePage() {
5 return (
6 <main>
7 <h1 className="mb-6 text-3xl font-bold">Latest posts</h1>
8 <div className="grid gap-6 md:grid-cols-3">
9 <WpQueryLoop postType="post" postsPerPage={3}>
10 <PostCard />
11 </WpQueryLoop>
12 </div>
13 </main>
14 )
15}During local development, the loop reads its posts from the CMS mock data. During export, ForgeWP recognizes the loop and its post hooks so the resulting theme can use WordPress's native post loop and post fields. The visual markup remains yours.
Why are the hooks inside `PostCard`?
The loop provides the current post through context. A card rendered outside the loop has no current post to read, so its values fall back to sample or default values instead.
Build a site navigation
For a menu managed in WordPress, use <WpMenu>. Its location identifies the menu location, and its class props let you style the generated navigation without replacing the data connection.
1import { WpMenu } from "@forgewp/react"
2
3export function SiteNavigation() {
4 return (
5 <WpMenu
6 location="primary"
7 className="flex items-center gap-6"
8 linkClassName="text-sm font-medium hover:underline"
9 submenuClassName="mt-2 space-y-2"
10 />
11 )
12}For local previews, define menu items in cms/menus.ts. The object key must match the component's location:
1import { defineWpMenus } from "@forgewp/react"
2
3export const menus = defineWpMenus({
4 primary: [
5 { title: "Home", url: "/" },
6 { title: "Journal", url: "/journal" },
7 { title: "Contact", url: "/contact" },
8 ],
9})The CMS file provides sample menu data for local development. The exported theme connects the primary location to WordPress navigation. A menu location does not create the menu items in WordPress; editors manage those items in the WordPress admin.
Add a UI primitive
You can also install a visual primitive from the shadcn registry and use it inside your own components:
pnpm forgewp add accordionThe CLI places the resulting source in src/components/ui/ and handles the dependencies declared by the upstream component. The exact component exports and composition API come from the generated source, so open that file and follow its exports when wiring it into your theme. This is especially useful for third-party registry components, whose APIs may change independently of ForgeWP.
The same separation of responsibilities applies: use the UI primitive for interaction and presentation, and keep WordPress data access in the ForgeWP hooks and components that the compiler understands.