ForgeWP

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 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/ where services is 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: project or case_study
  • An event directory: event or workshop
  • A restaurant website: menu_item or review
  • A corporate intranet: employee or department

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:

Table · 4 columns
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):

cms/mock-data.ts
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 mockData
Instant 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:

src/components/PostCard.tsx
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:

src/app/page.tsx
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:

src/app/page.tsx
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:

src/app/single.tsx
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:

Table · 3 columns
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.featuredImage
Custom Field
<?php get_post_meta($id, 'key', true); ?>
useWpCustomField('key') or post.customFields?.key
Query 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.

  1. 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.
  2. Sanitize HTML Content: When rendering post.content using dangerouslySetInnerHTML or <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).
  3. Use customFields for Mock Data: In cms/mock-data.ts, custom fields are declared under the customFields: { ... } object for each post or CPT item, which seamlessly feeds into useWpCustomField(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.