ForgeWP

Site Options, Theme Mods & Custom Fields

Consuming global site settings with useWpOption, customizer controls with useWpThemeMod, and custom metadata with useWpCustomField and WpRepeater.

Modern websites need more than just post titles and body content. You frequently need to read:

  1. Global Site Settings: Site title, tagline, admin email, or custom settings created for your theme.
  2. Theme Customizer Values (Theme Mods): Accent colors, social media links, or announcement banner toggles.
  3. Custom Post Metadata (PostMeta & ACF): Prices, review ratings, client names, or repeatable team member lists.

ForgeWP provides hooks and primitives for these categories. Their local-preview data and WordPress admin controls come from different configuration files, so use the matching helper for each kind of setting.


1. Global Site Options (useWpOption)

To read global WordPress settings (stored in the WordPress wp_options table), use useWpOption():

src/components/SiteFooter.tsx
1import { useWpOption } from "@forgewp/react"
2
3export function SiteFooter() {
4  const siteName = useWpOption("blogname", "ForgeWP Starter")
5  const siteTagline = useWpOption(
6    "blogdescription",
7    "High-performance WordPress Themes"
8  )
9  const copyrightYear = new Date().getFullYear()
10
11  return (
12    <footer className="border-t border-border bg-card py-12 text-center text-sm text-muted-foreground">
13      <p className="font-heading font-bold text-foreground">{siteName}</p>
14      <p className="mt-1">{siteTagline}</p>
15      <p className="mt-4 text-xs">
16        © {copyrightYear} {siteName}. Built with ForgeWP.
17      </p>
18    </footer>
19  )
20}

Standard WordPress Core Options

You can read any core WordPress option:

  • "blogname": Site Title (Settings → General)
  • "blogdescription": Tagline (Settings → General)
  • "admin_email": Administration Email
  • "posts_per_page": Blog pages show at most (Settings → Reading)

Custom Site Options Schema (cms/site-options.ts)

To define custom site-wide options, declare a schema in cms/site-options.ts. The field helpers provide editor labels, defaults, and input types:

cms/site-options.ts
1import { defineWpOptions, optionText } from "@forgewp/react/config"
2
3export const siteOptions = defineWpOptions({
4  announcement_text: optionText({
5    label: "Announcement text",
6    default: "Welcome to our site!",
7  }),
8  announcement_enabled: optionText({
9    label: "Show announcement (true or false)",
10    default: "true",
11  }),
12})

The starter imports this schema into wp.config.ts. In local preview, useWpOption() reads the field's default value; in WordPress, the compiler generates option access and admin settings from detected calls and the declared schema.

Then consume them directly in your components:

src/components/AnnouncementBar.tsx
1import { useWpOption } from "@forgewp/react"
2
3export function AnnouncementBar() {
4  const isEnabled = useWpOption("announcement_enabled", "true") === "true"
5  const text = useWpOption("announcement_text", "Welcome to our site!")
6
7  if (!isEnabled) return null
8
9  return (
10    <div className="bg-brand text-brand-foreground px-4 py-2 text-center text-xs font-bold">
11      {text}
12    </div>
13  )
14}

What the Compiler Emits

During pnpm export, useWpOption("key", "default") compiles to native PHP:

Generated PHP (Option Output)
1<?php echo esc_html( get_option( 'key', 'default' ) ); ?>

Use useWpThemeMod() to read a WordPress theme modification (get_theme_mod()). Use useWpOption() for social URLs and other site-wide values stored as WordPress options. Those are separate WordPress settings APIs.

src/components/SocialLinks.tsx
1import { useWpOption } from "@forgewp/react"
2import { Github, Twitter, Linkedin } from "lucide-react"
3
4export function SocialLinks() {
5  const github = useWpOption("social_github", "")
6  const twitter = useWpOption("social_twitter", "")
7  const linkedin = useWpOption("social_linkedin", "")
8
9  return (
10    <div className="flex items-center gap-4">
11      {github && (
12        <a
13          href={github}
14          target="_blank"
15          rel="noopener noreferrer"
16          className="text-muted-foreground hover:text-foreground"
17        >
18          <Github className="h-5 w-5" />
19        </a>
20      )}
21      {twitter && (
22        <a
23          href={twitter}
24          target="_blank"
25          rel="noopener noreferrer"
26          className="text-muted-foreground hover:text-foreground"
27        >
28          <Twitter className="h-5 w-5" />
29        </a>
30      )}
31      {linkedin && (
32        <a
33          href={linkedin}
34          target="_blank"
35          rel="noopener noreferrer"
36          className="text-muted-foreground hover:text-foreground"
37        >
38          <Linkedin className="h-5 w-5" />
39        </a>
40      )}
41    </div>
42  )
43}

To let an editor manage social URLs, declare them as site options. ForgeWP adds social_* options to its Social Media Customizer section:

cms/site-options.ts
1import { defineWpOptions, optionUrl } from "@forgewp/react/config"
2
3export const siteOptions = defineWpOptions({
4  social_github: optionUrl({
5    label: "GitHub URL",
6    default: "https://github.com/forgewp",
7  }),
8  social_linkedin: optionUrl({
9    label: "LinkedIn URL",
10    default: "https://linkedin.com/company/forgewp",
11  }),
12})

Save this schema in cms/site-options.ts and include it in wp.config.ts as options. The useWpThemeMod() hook reads theme modifications; cms/theme-mods.ts provides local mock/default values. ForgeWP generates controls for its built-in theme mods. For additional keys, register the corresponding Customizer controls in your theme integration.

For example, the theme's built-in footer_text modification can be read like this:

src/components/SiteFooter.tsx
1import { useWpThemeMod } from "@forgewp/react"
2
3export function SiteFooter() {
4  const footerText = useWpThemeMod("footer_text", "All rights reserved.")
5  return <footer>{footerText}</footer>
6}

3. Resolving Theme Directory Assets (useWpThemeUri)

When referencing static images, SVGs, or illustrations bundled inside your theme's assets/ or public/ directory, WordPress requires the dynamic template URL (get_template_directory_uri()).

Use useWpThemeUri() to ensure your image paths work both in local Vite preview and on any WordPress host:

src/components/BrandLogo.tsx
1import { useWpThemeUri } from "@forgewp/react"
2
3export function BrandLogo() {
4  const themeUri = useWpThemeUri()
5
6  return (
7    <img
8      src={`${themeUri}/assets/logo.svg`}
9      alt="Brand Logo"
10      className="h-8 w-auto"
11    />
12  )
13}
  • In Local Development: Resolves to "" so root-relative paths (/assets/logo.svg) load directly from your public folder.
  • In Production: Compiles to <?php echo esc_url( get_template_directory_uri() ); ?>/assets/logo.svg.

4. Post Metadata & Custom Fields (useWpCustomField)

To read custom post metadata (created through WordPress custom fields, meta boxes, or Advanced Custom Fields / ACF), use useWpCustomField():

src/components/ProjectMetadata.tsx
1import { useWpCustomField } from "@forgewp/react"
2
3export function ProjectMetadata() {
4  const client = useWpCustomField("client_name", "Acme Corp")
5  const year = useWpCustomField("project_year", "2026")
6  const budget = useWpCustomField("project_budget", "$25,000")
7
8  return (
9    <div className="grid grid-cols-3 gap-4 rounded-2xl border border-border bg-card p-6">
10      <div>
11        <span className="text-xs font-semibold text-muted-foreground uppercase">
12          Client
13        </span>
14        <p className="mt-1 font-bold text-foreground">{client}</p>
15      </div>
16      <div>
17        <span className="text-xs font-semibold text-muted-foreground uppercase">
18          Year
19        </span>
20        <p className="mt-1 font-bold text-foreground">{year}</p>
21      </div>
22      <div>
23        <span className="text-xs font-semibold text-muted-foreground uppercase">
24          Budget
25        </span>
26        <p className="mt-1 font-bold text-foreground">{budget}</p>
27      </div>
28    </div>
29  )
30}

Local Dev Simulation

In cms/mock-data.ts, add custom fields to any post under the customFields object:

cms/mock-data.ts
1{
2  id: 1,
3  title: "Next-Gen Fintech App",
4  customFields: {
5    client_name: "Stripe",
6    project_year: "2026",
7    project_budget: "$120,000",
8  },
9}

Compiled WordPress PHP

In your compiled WordPress theme, useWpCustomField('client_name') compiles into:

Generated PHP (PostMeta Output)
1<?php echo esc_html( get_post_meta( get_the_ID(), 'client_name', true ) ); ?>
Does `useWpCustomField()` work with Advanced Custom Fields (ACF) or MetaBox?

For fields stored as standard WordPress post metadata, useWpCustomField("client_name") reads the value by key. The hook returns a string and is not generic. Complex field return formats can vary by plugin; verify that the value stored in post meta matches the format your theme expects.


5. Complex Repeatable Data: <WpRepeater />

When you need to render repeatable subfields (such as an ACF Repeater or a list of team members, FAQs, or testimonial quotes), use the <WpRepeater /> primitive:

src/components/FaqSection.tsx
1import { WpRepeater } from "@forgewp/react"
2
3interface FaqItem {
4  question: string
5  answer: string
6}
7
8export function FaqSection() {
9  const defaultFaqs: FaqItem[] = [
10    {
11      question: "How does ForgeWP export themes?",
12      answer:
13        "ForgeWP compiles your React components directly into native PHP templates.",
14    },
15    {
16      question: "Can I use standard WordPress plugins?",
17      answer:
18        "The export runs inside WordPress. Verify each required plugin with your theme and editing workflow.",
19    },
20  ]
21
22  return (
23    <section className="mx-auto max-w-3xl px-6 py-16">
24      <h2 className="mb-8 text-center font-heading text-3xl font-bold text-foreground">
25        Frequently Asked Questions
26      </h2>
27
28      <div className="space-y-4">
29        <WpRepeater name="faq_items" defaultValue={defaultFaqs}>
30          {(faq, index) => (
31            <details
32              key={index}
33              className="group rounded-xl border border-border bg-card p-6"
34            >
35              <summary className="flex cursor-pointer list-none items-center justify-between font-bold text-foreground">
36                <span>{faq.question}</span>
37                <span className="transition group-open:rotate-180">↓</span>
38              </summary>
39              <p className="mt-4 text-sm leading-relaxed text-muted-foreground">
40                {faq.answer}
41              </p>
42            </details>
43          )}
44        </WpRepeater>
45      </div>
46    </section>
47  )
48}

How <WpRepeater /> Works

  1. In Local Development: Maps your render-prop function over the defaultValue array (or mock data in post.customFields.faq_items).
  2. At Compile Time: The compiler uses a JavaScript Proxy to inspect the subfields accessed (faq.question, faq.answer) and generates an optimized WordPress loop (get_post_meta or ACF have_rows()) in PHP.

Summary

You have mastered the complete WordPress Data Layer in ForgeWP:

  1. Single Post Primitives (useWpTitle, useWpContent, useWpExcerpt, useWpDate, useWpAuthor, <BlockArea />).
  2. Query Loops (<WpQueryLoop> for WordPress-rendered lists, useWpQuery() for interactive client pagination).
  3. Menus & Navigation (<WpMenu />, useWpMenu(), <WpLink />, useLocation()).
  4. Site Settings & Metadata (useWpOption, useWpThemeMod, useWpCustomField, <WpRepeater />).

Next, we proceed to Assets, Images & Media Handling, where you will learn how to use <WpImage>, handle WordPress featured images, and understand the build-time WebP pipeline.