ForgeWP

Sync Routes and Custom Post Type Templates

Generate local preview pages from menus, understand route-sync overwrite behavior, and scaffold custom post type views.

This guide covers two related workflows:

  • sync:routes reads local menu URLs and adds preview routes and starter page components.
  • make:post-type adds mock content for a custom post type (CPT). During export, ForgeWP infers CPTs from mock-data keys and registers them; sync:routes can then scaffold a single-item template wrapper for each discovered type.

These commands work in your theme project; they do not create pages or post types on a live WordPress site until you export and install the theme.

Generate page routes from menus

Route synchronization reads cms/menus.ts when present, or falls back to cms/menus.json. It uses local menu URLs such as /about-us to scaffold page components and add routes to src/app/routes.tsx.

Here is a small typed menu example:

cms/menus.ts
1import { defineWpMenus } from "@forgewp/react"
2
3export const menus = defineWpMenus({
4  primary: [
5    { title: "Home", url: "/" },
6    { title: "About Us", url: "/about-us" },
7  ],
8})

Run the sync command from the theme root:

Terminal
pnpm forgewp sync:routes

For the /about-us item, the command derives a component name from the menu title, creates a file such as src/app/pages/AboutUsPage.tsx, and, if src/app/routes.tsx exists, adds its import and local route before the fallback route. Inspect the generated component and route before customizing them. Route synchronization reads menu data; it does not add the menu item to a WordPress admin menu by itself.

What does `--force` do?

If a generated page component already exists, sync:routes normally skips it. Passing --force lets the command overwrite generated page source with its scaffold again. Commit or back up your edits before using it.

When there are no menu locations, sync:routes may create a starter cms/menus.json before scaffolding routes. If your project uses typed menus, keep cms/menus.ts as the source of truth and review any generated JSON file before using it.

Add a custom post type

A custom post type (CPT) is a WordPress content type other than the built-in post or page, such as portfolio or event. For the content-model background, see The WordPress Content Model.

Scaffold sample local entries for a portfolio post type:

Terminal
pnpm forgewp make:post-type portfolio --customFields=client_name

The command adds two sample entries under a portfolio key in cms/mock-data.ts (or the legacy JSON file if that is the project's active source). The generated entries have the same shape as your other mock posts. A simplified excerpt looks like this:

cms/mock-data.ts (generated portfolio entry)
1portfolio: [
2  {
3    id: 1,
4    title: "Sample Portfolio Item 1",
5    excerpt: "This is a custom portfolio post seeded by ForgeWP CLI.",
6    content: "<p>Edit this sample in cms/mock-data.ts.</p>",
7    date: "May 10, 2026",
8    author: "ForgeWP CLI",
9    featuredImage: "https://picsum.photos/seed/portfolio1/1200/630",
10    customFields: {
11      client_name: "[Seeded client_name 1]",
12    },
13  },
14]

The example shows one of the two generated entries; the actual date and placeholder values may vary. Because the key is portfolio, use that exact post type in a loop. The generated client_name value is mock content for local preview, not a live WordPress field value:

src/app/page.tsx (portfolio query example)
1import { WpQueryLoop, useWpCustomField, useWpTitle } from "@forgewp/react"
2
3function PortfolioCard() {
4  const title = useWpTitle()
5  const client = useWpCustomField("client_name", "Client name")
6
7  return (
8    <article>
9      <h2>{title}</h2>
10      <p>Client: {client}</p>
11    </article>
12  )
13}
14
15export default function HomePage() {
16  return (
17    <WpQueryLoop postType="portfolio" postsPerPage={6}>
18      <PortfolioCard />
19    </WpQueryLoop>
20  )
21}

During export, ForgeWP infers and registers custom post types from non-core mock-data keys. In wp.config.ts, the matching postTypes entry customizes the labels and admin icon; it can also declare a type with no seed entries:

wp.config.ts (postTypes setting)
1import { defineConfig } from "@forgewp/compiler/define-config"
2
3export default defineConfig({
4  name: "Studio",
5  slug: "studio",
6  version: "1.0.0",
7  description: "Studio portfolio theme",
8  textDomain: "studio",
9  postTypes: {
10    portfolio: {
11      labels: { singular: "Project", plural: "Projects" },
12      icon: "dashicons-portfolio",
13    },
14  },
15})

Merge the postTypes entry into your existing wp.config.ts; do not replace your theme's current configuration. make:post-type creates local mock items. The compiler uses the corresponding mock-data key to discover and register the type during export.

To place single-item permalinks under a custom path, configure the CPT rewrite rule. For example, if /projects is an authored WordPress Page, disable the CPT archive so the archive does not compete for that route:

wp.config.ts (CPT permalink base)
1postTypes: {
2  project: {
3    labels: { singular: "Project", plural: "Projects" },
4    hasArchive: false,
5    rewrite: { slug: "projects", withFront: false },
6  },
7},

The example gives each project its own /projects/<post-slug> permalink while leaving /projects available to the authored Page. The generated single template is src/app/single-project.tsx; use useWpPermalink() in links so WordPress supplies the post's current URL, including after an editor changes its slug. ForgeWP compares the effective CPT routing signature and refreshes rewrite rules once when it changes. This includes restoring default settings or removing the final theme-discovered CPT. Reordering equivalent declarations does not request another refresh.

rewrite may also be set to false to disable pretty permalinks for that CPT. When omitted, WordPress's default rewrite behavior is retained. The optional withFront value controls whether the site's permalink front prefix is included.

Read export routing diagnostics

During PHP generation, ForgeWP compares CPT archive bases with Page paths collected from compiled Page templates, menus, and supported static route declarations. If both claim the same path, export prints a potential route collision with the CPT key and path. Move one to a different path, or use hasArchive: false as above when the Page should own the listing URL. public: false alone does not guarantee the type reserves no archive rewrite rules.

The comparison uses the declared archive base before any site-level permalink front prefix. It cannot inspect existing WordPress Pages, plugin-owned registrations, custom rewrite filters, or the site's actual permalink settings. Treat the warning as a source-level review prompt and verify the actual routes on WordPress. It does not detect every possible single-post/Page URL collision.

Explicit rewrite/archive configuration also prints a routing summary and a reminder to review redirects when changing installed URLs. The CPT base is developer-owned configuration; an editor's post-slug change affects that item's URL. Neither export nor the rewrite refresh deletes saved Pages or creates a redirect policy. Redirects for previously published URLs need a separate, deliberate migration step.

See WordPress's post-type registration reference and rewrite rule implementation for archive and permalink-front behavior.

During export, ForgeWP removes stale generated template-*.html and single-*.html cache files before rendering the current sources. This keeps removed local routes out of a newly generated theme; it does not delete WordPress Pages that were already created on an installed site.

Scaffold a single-item preview

sync:routes also discovers custom post type keys in the mock data. For portfolio, it can scaffold:

Table · 2 columns
File
Role
src/app/single-portfolio.tsx
Thin entry component recognized as the custom post type's WordPress single template.
src/app/pages/SinglePortfolioPage.tsx
The editable React view used by that wrapper.
src/app/routes.tsx
A local preview route with an :id parameter, when this file exists.

The local preview route uses an ID-shaped path such as /portfolio/:id. On the live site, WordPress chooses single-portfolio.php according to the CPT permalink and template hierarchy. The top-level src/app/single-portfolio.tsx wrapper is what the export renderer recognizes for that PHP template.

If you need a custom CPT listing, create a default-export component in src/app/archive-portfolio.tsx. The export renderer recognizes top-level archive-<post-type>.tsx files and generates the corresponding PHP hierarchy template. sync:routes scaffolds the single-item view; it does not create the archive component for you. It skips existing generated views by default; --force may replace them.

What happens to WordPress Pages

During export, ForgeWP can generate PHP setup code that creates WordPress Pages for compiled custom page templates and local routes. It matches menu titles or URLs to template names when possible and otherwise derives a slug from the template filename. Existing pages are matched by slug and may have their template assigned by the generated setup. Review the Pages list and template assignments after activating a new theme, especially on a site with existing content.

Use Custom Page Templates for a single WordPress Page layout, and Routes and WordPress Templates for the distinction between local Vite routes and WordPress templates.