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:
- A visitor navigates to a URL (e.g.,
https://example.com/category/engineering/). - WordPress parses the URL through its internal rewrite rules and queries the database.
- WordPress determines what kind of data was requested (e.g., "This is an archive query for the Category taxonomy term 'engineering'").
- WordPress searches your theme directory for template files in order of highest specificity to lowest specificity.
- The first matching template file found in your theme is executed to render the response.
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:
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:
ForgeWP React Source Generated WordPress Template When WordPress Uses It src/app/page.tsxfront-page.php, index.phpThe website homepage ( /) and universal fallback template.src/app/single.tsxsingle.phpAny individual blog post or custom post type article. src/app/archive.tsxarchive.phpCategory feeds, tag listings, author archives, and date listings. src/app/404.tsx404.phpAny URL that does not match an existing post, page, or archive. src/app/pages/AboutUs.tsxpage-about-us.phpA custom page template scaffolded with pnpm forgewp make:page AboutUs.src/app/single-project.tsxsingle-project.phpSpecialized template for a Custom Post Type named project.src/app/archive-project.tsxarchive-project.phpDedicated 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:
- WordPress checks if Settings → Reading is set to "Your latest posts" or "A static page".
- Regardless of setting, WordPress first looks for
front-page.php. - ForgeWP compiles
src/app/page.tsxdirectly intofront-page.php(andindex.php). - Result: Your
src/app/page.tsxcomponent is rendered.
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:
- WordPress recognizes that the query is for a single post of post type
post. - WordPress checks for
single-post-mastering-react.php(not found). - WordPress checks for
single-post.php(not found). - WordPress checks for
single.php(found!). - Result: Your
src/app/single.tsxcomponent is rendered with the post's full content.
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:
- WordPress identifies this as a taxonomy archive for the category term
engineering. - WordPress looks for
category-engineering.php(not found). - WordPress looks for
category.php(not found). - WordPress looks for
archive.php(found!). - Result: Your
src/app/archive.tsxcomponent renders, populated automatically with all posts tagged with the "Engineering" category.
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}Scenario D: A Broken or Missing Link (https://example.com/expired-campaign/)
When a visitor requests a URL that does not exist in the database:
- WordPress returns a 404 response header.
- WordPress checks for
404.phpin your active theme (found!). - Result: Your
src/app/404.tsxcomponent is rendered.
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:
src/app/page.tsx(Homepage & universal fallback)src/app/single.tsx(Individual articles and content items)src/app/404.tsx(Not found screen) You only need to create specialized templates (likearchive.tsxorsingle-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.