The WordPress Content Model
Posts, Pages, Custom Post Types, Taxonomies, and Postmeta — demystified for modern React developers.
If you are coming from pure React, Next.js, or Remix, you are likely used to thinking of content as JSON documents fetched from headless APIs, database rows queried with an ORM like Prisma, or Markdown files parsed with MDX.
WordPress organizes content through a battle-tested, relational content model developed over two decades. Understanding this structure is the secret to building resilient, elegant themes with ForgeWP.
No SQL or PHP Required
As a ForgeWP developer, you do not write MySQL queries or PHP loops. ForgeWP allows you to simulate WordPress content offline in pure TypeScript, and compile your React components into native WordPress theme templates automatically.
The 5 Core Content Primitives
At its core, almost all dynamic content in WordPress belongs to five primary primitives:
WordPress Content Model
├── 1. Posts Time-stamped, reverse-chronological articles
├── 2. Pages Hierarchical, static evergreen content
├── 3. Custom Post Types Specialized domain models (e.g. portfolio, events)
├── 4. Taxonomies Classification systems (categories & tags)
└── 5. Post Meta Key-value custom fields (ACF / metadata)1. Posts (Articles & News)
Posts represent time-sensitive, chronological content such as blog entries, news items, product announcements, or case studies.
Key characteristics of a Post:
- Time-dependent: Has a published date and timestamp.
- Taxonomy-bound: Always classified under one or more Categories and zero or more Tags.
- Author attribution: Linked to a registered WordPress user.
- Featured Image: An optional primary banner/thumbnail image.
- Excerpt: A concise summary used in archive previews and SEO meta tags.
2. Pages (Evergreen Static Content)
Pages are designed for timeless, static content that does not expire or belong in a reverse-chronological feed. Typical examples include:
/about— Company history and team bio./services— Offerings and pricing./contact— Contact forms and office locations./privacy-policy— Legal documents.
Unlike posts, pages:
- Are hierarchical: A page can have a parent page (e.g.,
/services/web-development/whereservicesis the parent). - Do not use categories or tags by default.
- Can be assigned custom page templates (which you author as React components in
src/app/pages/).
3. Custom Post Types (CPTs)
When standard Posts and Pages are not specific enough for your domain model, WordPress lets you register Custom Post Types (CPTs).
Common real-world CPT examples:
- A portfolio site:
projectorcase_study - An event directory:
eventorworkshop - A restaurant website:
menu_itemorreview - A corporate intranet:
employeeordepartment
In ForgeWP, CPTs behave identically to standard posts in your React components, but you can render them through dedicated templates like src/app/single.tsx or specialized hierarchy templates.
4. Taxonomies & Terms
Taxonomies are grouping mechanisms used to classify posts and custom post types:
Taxonomy Type Structure Built-in Example Custom Example Hierarchical Nested parent/child trees category (e.g., Engineering > React)department (e.g., Design > UI)Flat / Non-hierarchical Freeform keyword tags post_tag (e.g., performance, css)skill (e.g., typescript, graphql) |
|---|
Each individual item within a taxonomy is called a Term (e.g., "Design" is a term inside the "category" taxonomy).
5. Post Meta & Custom Fields
In modern web development, standard titles and body text are rarely enough. You frequently need structured metadata:
- A real estate listing needs:
price,bedrooms,square_feet. - An event post needs:
event_date,venue_address,ticket_url. - A book review needs:
isbn,rating,publication_year.
WordPress stores this extra structured data as Post Meta (key-value pairs attached to a post ID). In WordPress production sites, developers often manage these fields using plugins like Advanced Custom Fields (ACF) or native Gutenberg custom meta boxes.
Is rendering WordPress post HTML in React automatically safe?
Treat HTML as trusted only when it comes from a source and author role you trust. WordPress applies capability-dependent filtering when content is saved, and the_content applies display filters, but users with unfiltered_html, plugins, imports, or custom integrations can change what is stored. Do not pass arbitrary user-submitted HTML to dangerouslySetInnerHTML; sanitize or escape content according to its source and use case.
Simulating Content in ForgeWP
During local development, ForgeWP provides zero-backend simulation through cms/mock-data.ts. The schema uses defineWpPosts from @forgewp/react, which accepts a dictionary keyed by post type (post, page, and any custom post types such as portfolio or project):
1import { defineWpPosts } from "@forgewp/react"
2
3export const mockData = defineWpPosts({
4 // 1. Standard Blog Posts
5 post: [
6 {
7 id: 1,
8 slug: "the-art-of-less-minimalist-interiors",
9 title: "The Art of Less: Finding Beauty in Minimalist Interiors",
10 excerpt:
11 "Discover the core principles behind calm, considered interiors.",
12 content:
13 "<p>Minimalism in the home is intentional selection: fewer objects, better materials, clearer light.</p>",
14 date: "2026-05-11T09:00:00Z",
15 author: "Alex River",
16 featuredImage:
17 "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?w=1200",
18 category: "Interior Design",
19 categorySlug: "interior-design",
20 readTime: "4 min read",
21 featured: true,
22 customFields: {
23 reading_time: "4 min read",
24 client_name: "Minimal Living Co",
25 project_budget: "$25,000",
26 },
27 _terms: {
28 category: [{ id: 1, slug: "interior-design", name: "Interior Design" }],
29 },
30 },
31 ],
32
33 // 2. Evergreen Pages
34 page: [
35 {
36 id: 10,
37 slug: "about",
38 title: "About Our Studio",
39 content:
40 "<p>We design high-performance digital experiences powered by ForgeWP.</p>",
41 },
42 ],
43
44 // 3. Custom Post Types (e.g., portfolio, project, event)
45 portfolio: [
46 {
47 id: 1,
48 title: "Sample Portfolio Item",
49 excerpt: "A custom portfolio post seeded dynamically via ForgeWP.",
50 content:
51 "<p>Welcome to your custom portfolio post template! Edit this in cms/mock-data.ts.</p>",
52 date: "August 23, 2026",
53 author: "ForgeWP CLI",
54 featuredImage: "https://picsum.photos/seed/portfolio1/1200/630",
55 customFields: {
56 client: "Studio Minimal",
57 budget: "$45,000",
58 },
59 },
60 ],
61})
62
63export default mockDataInstant Hot Reloading
When you modify entries or customFields in cms/mock-data.ts, Vite's development server instantly updates your browser preview without requiring a full page refresh or server restart.
Consuming Content in React Components
ForgeWP provides intuitive React hooks and primitives through @forgewp/react that allow you to consume posts, pages, and metadata seamlessly.
Listing Posts with <WpQueryLoop /> or useWpQuery
ForgeWP provides two complementary querying patterns depending on your performance and interactivity goals:
<WpQueryLoop />(Useful for server-rendered lists): Compiles into a WordPress query loop during export. The loop itself does not require a client-side query bundle; child components may still add interactive islands or other scripts.useWpQuery()(For Client Interactivity): A powerful React hook for components that require client-side interactivity, live search filters, AJAX pagination, or dynamic sorting without page reloads.
Pattern 1: Declarative Theme Loop with <WpQueryLoop /> (Static-First)
If you are building standard themes (homepages, category feeds, author archives) that should compile to native WordPress PHP with zero runtime JavaScript, use <WpQueryLoop />. Child components automatically access the current post's properties via context:
1import {
2 useWpTitle,
3 useWpExcerpt,
4 useWpPermalink,
5 useWpDate,
6 useWpFeaturedImage,
7} from "@forgewp/react"
8
9export function PostCard() {
10 const title = useWpTitle()
11 const excerpt = useWpExcerpt()
12 const permalink = useWpPermalink()
13 const date = useWpDate()
14 const featuredImage = useWpFeaturedImage()
15
16 return (
17 <article className="rounded-xl border border-border bg-card p-5">
18 {featuredImage && (
19 <img
20 src={featuredImage}
21 alt={title}
22 className="mb-4 h-44 w-full rounded-lg object-cover"
23 />
24 )}
25 <div className="mb-2 text-xs text-muted-foreground">{date}</div>
26 <h2 className="mb-2 text-xl font-semibold">
27 <a href={permalink} className="hover:underline">
28 {title}
29 </a>
30 </h2>
31 <p className="line-clamp-3 text-sm text-muted-foreground">{excerpt}</p>
32 </article>
33 )
34}Then in your homepage or archive template:
1import { WpQueryLoop } from "@forgewp/react"
2import { PostCard } from "@/components/PostCard"
3
4export default function HomePage() {
5 return (
6 <main className="mx-auto max-w-5xl px-4 py-12">
7 <h1 className="mb-8 text-3xl font-bold tracking-tight">
8 Latest Dispatches
9 </h1>
10 <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
11 <WpQueryLoop postType="post" postsPerPage={6}>
12 <PostCard />
13 </WpQueryLoop>
14 </div>
15 </main>
16 )
17}Pattern 2: Hook-Based Querying with useWpQuery (Interactive)
When building an interactive search feed, category filter bar, or infinite scroll listicle, use the useWpQuery hook:
1import { useWpQuery, type WpPost } from "@forgewp/react"
2
3export default function HomePage() {
4 const { posts, loading, error } = useWpQuery({
5 postType: "post",
6 postsPerPage: 6,
7 })
8
9 if (loading) {
10 return (
11 <div className="py-12 text-center text-muted-foreground">
12 Loading dispatches…
13 </div>
14 )
15 }
16
17 if (error) {
18 return (
19 <div className="py-12 text-center text-red-500">
20 Failed to load posts.
21 </div>
22 )
23 }
24
25 return (
26 <main className="mx-auto max-w-5xl px-4 py-12">
27 <h1 className="mb-8 text-3xl font-bold tracking-tight">
28 Latest Dispatches
29 </h1>
30
31 <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
32 {posts.map((post: WpPost) => {
33 const imageUrl =
34 typeof post.featuredImage === "string"
35 ? post.featuredImage
36 : post.featuredImage?.url
37
38 return (
39 <article
40 key={post.id}
41 className="rounded-xl border border-border bg-card p-5"
42 >
43 {imageUrl && (
44 <img
45 src={imageUrl}
46 alt={post.title || "Post thumbnail"}
47 className="mb-4 h-44 w-full rounded-lg object-cover"
48 />
49 )}
50 <div className="mb-2 text-xs text-muted-foreground">
51 {new Date(post.date).toLocaleDateString("en-US", {
52 month: "short",
53 day: "numeric",
54 year: "numeric",
55 })}
56 </div>
57 <h2 className="mb-2 text-xl font-semibold">{post.title}</h2>
58 <p className="line-clamp-3 text-sm text-muted-foreground">
59 {post.excerpt}
60 </p>
61 </article>
62 )
63 })}
64 </div>
65 </main>
66 )
67}Accessing Singular Post Data & Custom Fields
In singular templates (like src/app/single.tsx) or custom page templates, ForgeWP provides context-aware hooks that extract the active post's fields without manual prop drilling:
1import {
2 useWpTitle,
3 useWpContent,
4 useWpAuthor,
5 useWpDate,
6 useWpFeaturedImage,
7 useWpCustomField,
8} from "@forgewp/react"
9
10export default function SinglePostPage() {
11 const title = useWpTitle()
12 const content = useWpContent()
13 const author = useWpAuthor()
14 const date = useWpDate()
15 const featuredImage = useWpFeaturedImage()
16 const readingTime = useWpCustomField("reading_time", "3 min read")
17
18 return (
19 <article className="mx-auto max-w-3xl px-4 py-16">
20 {featuredImage && (
21 <img
22 src={featuredImage}
23 alt={title}
24 className="mb-8 h-72 w-full rounded-2xl object-cover"
25 />
26 )}
27 <h1 className="mb-4 text-4xl font-extrabold tracking-tight">{title}</h1>
28 <div className="mb-8 flex items-center gap-4 text-sm text-muted-foreground">
29 <span>By {author}</span>
30 <span>•</span>
31 <span>{date}</span>
32 <span>•</span>
33 <span>{readingTime}</span>
34 </div>
35 <div
36 className="prose dark:prose-invert max-w-none"
37 dangerouslySetInnerHTML={{ __html: content }}
38 />
39 </article>
40 )
41}Why `dangerouslySetInnerHTML`?
In React, injecting raw HTML strings stored in a database (such as Gutenberg post content or classic paragraph tags) requires the dangerouslySetInnerHTML={{ __html: content }} property. Despite the intimidating name, WordPress post content rendered on your own site is safe because WordPress automatically sanitizes HTML when authors save posts in the editor.
Mental Model Comparison
Here is how common content operations translate between traditional WordPress PHP and modern ForgeWP React:
Operation Traditional WordPress PHP Modern ForgeWP React Post Title <?php the_title(); ?>useWpTitle() or {post.title}Post Body <?php the_content(); ?>useWpContent() or <div dangerouslySetInnerHTML={{ __html: post.content }} />Post Date <?php echo get_the_date(); ?>useWpDate() or {new Date(post.date).toLocaleDateString()}Author Name <?php the_author(); ?>useWpAuthor() or {post.author}Featured Image <?php the_post_thumbnail('large'); ?>useWpFeaturedImage() or post.featuredImageCustom Field <?php get_post_meta($id, 'key', true); ?>useWpCustomField('key') or post.customFields?.keyQuery Loop while (have_posts()) : the_post(); ... endwhile;<WpQueryLoop postType="post"><Card /></WpQueryLoop> or useWpQuery |
|---|
Best Practices & Common Traps
Always Guard Against Missing Optional Data
In WordPress, a post might not have a featured image, an excerpt, or specific custom fields populated by an author. Always use optional chaining (post.featuredImage) or fallback values (post.excerpt || post.content.slice(0, 160)) in your React components to prevent rendering errors.
- Avoid Hardcoded IDs: Never write logic like
if (post.id === 42). Post IDs change between environments (e.g., local mock database vs staging vs production WordPress). Always query or branch based on slugs, categories, or custom meta flags. - Sanitize HTML Content: When rendering
post.contentusingdangerouslySetInnerHTMLor<BlockArea />, remember that WordPress content comes formatted as raw HTML strings containing Gutenberg block markup or classic paragraph tags (see Styling Gutenberg Content & Editor Parity). - Use
customFieldsfor Mock Data: Incms/mock-data.ts, custom fields are declared under thecustomFields: { ... }object for each post or CPT item, which seamlessly feeds intouseWpCustomField(key)(see Site Options, Theme Mods & Custom Fields).
Next Step: Template Routing
Proceed to The Template Hierarchy Demystified to learn how WordPress resolves URLs into templates, and how ForgeWP maps your React components 1:1.