ForgeWP

The Template Hierarchy Demystified

How WordPress resolves URLs into templates, and how ForgeWP maps supported app files to theme templates.

If you have built applications with Next.js App Router, Remix, or Vite, you are accustomed to file-system routing: if you place a file at src/app/blog/[slug]/page.tsx, incoming requests to /blog/my-first-post directly render that specific component.

WordPress operates on an inverted mental model known as the Template Hierarchy. Understanding this hierarchy gives you complete control over how URLs map to your React components.


File-System Routing vs. Template Hierarchy

In WordPress, URLs are not mapped to hardcoded file paths on disk. Instead:

  1. A visitor navigates to a URL (e.g., https://example.com/category/engineering/).
  2. WordPress parses the URL through its internal rewrite rules and queries the database.
  3. WordPress determines what kind of data was requested (e.g., "This is an archive query for the Category taxonomy term 'engineering'").
  4. WordPress searches your theme directory for template files in order of highest specificity to lowest specificity.
  5. The first matching template file found in your theme is executed to render the response.
Routing Architecture Comparison
Traditional File-System Routing:
  URL: /blog/post-1
  └── Loads: src/app/blog/page.tsx

WordPress Template Hierarchy:
  URL: /category/react
  ├── 1. category-react.php      (Specific category slug)
  ├── 2. category-6.php          (Specific term ID)
  ├── 3. category.php            (General category template)
  ├── 4. archive.php             (General archive template)
  └── 5. index.php               (Universal fallback template)
Why the Hierarchy Matters

The Template Hierarchy ensures that your site never breaks if a specialized template is missing. If you don't provide a dedicated template for a specific category, WordPress gracefully falls back to category.php, then archive.php, and finally index.php.


The Visual Hierarchy Decision Tree

The following diagram illustrates the decision tree WordPress follows when choosing which template file to load:

Flowchart
Rendering diagram...

ForgeWP Source Mapping

ForgeWP honors the WordPress template hierarchy while giving you a clean, modular React component architecture in src/app/.

When you run pnpm export, the ForgeWP compiler translates your React route components into native WordPress template files:

Table · 3 columns
ForgeWP React Source
Generated WordPress Template
When WordPress Uses It
src/app/page.tsx
front-page.php, index.php
The website homepage (/) and universal fallback template.
src/app/single.tsx
single.php
Any individual blog post or custom post type article.
src/app/archive.tsx
archive.php
Category feeds, tag listings, author archives, and date listings.
src/app/404.tsx
404.php
Any URL that does not match an existing post, page, or archive.
src/app/pages/AboutUs.tsx
page-about-us.php
A custom page template scaffolded with pnpm forgewp make:page AboutUs.
src/app/single-project.tsx
single-project.php
Specialized template for a Custom Post Type named project.
src/app/archive-project.tsx
archive-project.php
Dedicated archive catalog for the project post type.
Local Routing with `src/app/routes.tsx`

In local development mode (pnpm dev), Vite previews these templates using src/app/routes.tsx. When you create a new template with pnpm forgewp make:page AboutUs, import and mount it under <Switch> in src/app/routes.tsx to preview it instantly at http://localhost:5173/about-us.

How do custom 404 pages and search results work in ForgeWP?

src/app/404.tsx maps to the generated 404.php template. The current compiler does not generate a dedicated search.php from src/app/search.tsx; WordPress search requests continue through the standard hierarchy and may fall back to the generated generic template. Do not rely on a search.tsx file being compiled as a search template.


Step-by-Step Resolution Walkthroughs

Let's walk through four real-world scenarios to see how WordPress matches incoming URLs to your compiled ForgeWP templates.

Scenario A: The Homepage (https://example.com/)

When a user lands on the root domain:

  1. WordPress checks if Settings → Reading is set to "Your latest posts" or "A static page".
  2. Regardless of setting, WordPress first looks for front-page.php.
  3. ForgeWP compiles src/app/page.tsx directly into front-page.php (and index.php).
  4. Result: Your src/app/page.tsx component is rendered.
src/app/page.tsx
1import { useWpOption, useWpQuery, type WpPost } from "@forgewp/react"
2
3export default function HomePage() {
4  const siteTitle = useWpOption("blogname", "Velvet & Oak")
5  const tagline = useWpOption("blogdescription", "React & Tailwind Themes")
6  const { posts } = useWpQuery({ postType: "post", postsPerPage: 6 })
7
8  return (
9    <main className="mx-auto max-w-5xl px-4 py-16">
10      <header className="mb-12">
11        <h1 className="text-4xl font-extrabold">{siteTitle}</h1>
12        <p className="text-lg text-muted-foreground">{tagline}</p>
13      </header>
14
15      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
16        {posts.map((post: WpPost) => (
17          <article
18            key={post.id}
19            className="rounded-xl border border-border bg-card p-6"
20          >
21            <h2 className="mb-2 text-xl font-semibold">{post.title}</h2>
22            <p className="text-sm text-muted-foreground">{post.excerpt}</p>
23          </article>
24        ))}
25      </div>
26    </main>
27  )
28}

Scenario B: A Blog Article (https://example.com/mastering-react/)

When a visitor views an article with the post slug mastering-react:

  1. WordPress recognizes that the query is for a single post of post type post.
  2. WordPress checks for single-post-mastering-react.php (not found).
  3. WordPress checks for single-post.php (not found).
  4. WordPress checks for single.php (found!).
  5. Result: Your src/app/single.tsx component is rendered with the post's full content.
src/app/single.tsx
1import {
2  useWpTitle,
3  useWpContent,
4  useWpAuthor,
5  useWpDate,
6} from "@forgewp/react"
7
8export default function SinglePostPage() {
9  const title = useWpTitle()
10  const content = useWpContent()
11  const author = useWpAuthor()
12  const date = useWpDate()
13
14  return (
15    <article className="mx-auto max-w-3xl px-4 py-16">
16      <header className="mb-8">
17        <h1 className="mb-3 text-4xl font-bold tracking-tight">{title}</h1>
18        <p className="text-sm text-muted-foreground">
19          Published on {date} by {author}
20        </p>
21      </header>
22      <div
23        className="prose dark:prose-invert max-w-none"
24        dangerouslySetInnerHTML={{ __html: content }}
25      />
26    </article>
27  )
28}

Scenario C: A Category Archive (https://example.com/category/engineering/)

When a user clicks on the "Engineering" category:

  1. WordPress identifies this as a taxonomy archive for the category term engineering.
  2. WordPress looks for category-engineering.php (not found).
  3. WordPress looks for category.php (not found).
  4. WordPress looks for archive.php (found!).
  5. Result: Your src/app/archive.tsx component renders, populated automatically with all posts tagged with the "Engineering" category.
src/app/archive.tsx
1import { useWpTitle, useWpQuery, type WpPost } from "@forgewp/react"
2
3export default function ArchivePage() {
4  // In an archive template, useWpTitle() automatically evaluates to the current category or term title
5  const archiveTitle = useWpTitle()
6  const { posts, loading } = useWpQuery({ postType: "post" })
7
8  return (
9    <main className="mx-auto max-w-5xl px-4 py-16">
10      <h1 className="mb-8 text-3xl font-bold tracking-tight">
11        Browsing: {archiveTitle}
12      </h1>
13
14      {loading ? (
15        <p className="text-muted-foreground">Loading archive entries…</p>
16      ) : (
17        <div className="grid gap-6 md:grid-cols-2">
18          {posts.map((post: WpPost) => (
19            <div
20              key={post.id}
21              className="rounded-xl border border-border bg-card p-6"
22            >
23              <h2 className="mb-2 text-xl font-semibold">{post.title}</h2>
24              <p className="text-sm text-muted-foreground">{post.excerpt}</p>
25            </div>
26          ))}
27        </div>
28      )}
29    </main>
30  )
31}

When a visitor requests a URL that does not exist in the database:

  1. WordPress returns a 404 response header.
  2. WordPress checks for 404.php in your active theme (found!).
  3. Result: Your src/app/404.tsx component is rendered.
src/app/404.tsx
1export default function NotFoundPage() {
2  return (
3    <main className="flex min-h-[60vh] flex-col items-center justify-center px-4 text-center">
4      <span className="mb-4 text-6xl font-extrabold text-primary">404</span>
5      <h1 className="mb-2 text-2xl font-bold">Page Not Found</h1>
6      <p className="mb-6 text-muted-foreground">
7        The link you followed may be broken or the page has been moved.
8      </p>
9      <a
10        href="/"
11        className="rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-primary-foreground"
12      >
13        Return to Homepage
14      </a>
15    </main>
16  )
17}

The Golden Rule for Theme Authors

Start Broad, Specialize Only When Needed

When starting a new theme, you only need three core files:

  1. src/app/page.tsx (Homepage & universal fallback)
  2. src/app/single.tsx (Individual articles and content items)
  3. src/app/404.tsx (Not found screen) You only need to create specialized templates (like archive.tsx or single-project.tsx) when a specific content section requires a fundamentally different visual layout or interaction model.

Next Up: Authoring with React

Proceed to React Basics For Theme Authors to master decomposing WordPress templates into modern, type-safe React function components and styling with Tailwind CSS 4.