Local Assets, SVGs & Dynamic Icons
Bundling local theme assets in public/ vs src/assets/, resolving dynamic runtime paths with useWpThemeUri, and using the tree-shaken <WpIcon> primitive.
While post featured images and user uploads reside in the WordPress Media Library, theme-specific branding—such as site logos, decorative vector artwork, UI sound effects, and icon sets—lives inside your theme repository.
ForgeWP provides a clean dual-folder strategy for static files, automatic WordPress theme URI resolution, the useWpThemeUri() hook for dynamic runtime paths, and an integrated zero-runtime <WpIcon> component.
1. Where to Store Local Theme Assets
In a ForgeWP project, static assets live in one of two directories depending on whether you want Vite to bundle and hash them:
my-theme/
├── public/ # Static Root Assets (copied as-is)
│ ├── favicon.ico
│ ├── robots.txt
│ └── screenshot.png # WordPress theme preview card (1200x900)
│
├── src/
│ ├── assets/ # Bundled Assets (processed by Vite)
│ │ ├── logo.svg
│ │ ├── hero-pattern.png
│ │ └── vectors/
│ │ └── illustration.svgOption A: public/ (Unprocessed Static Files)
Any file placed inside public/ is copied directly into the root of your compiled WordPress theme ZIP without changing its filename.
- Best for:
favicon.ico,robots.txt, WordPressscreenshot.png, or files that must maintain fixed, predictable URLs. - Referencing in Code: Reference directly with a leading slash:
1import { WpImage } from "@forgewp/react"
2
3 export function SiteLogo() {
4 return (
5 <WpImage
6 src="/assets/logo.svg"
7 alt="Brand Logo"
8 width={180}
9 height={40}
10 className="h-8 w-auto"
11 />
12 )
13 }Option B: src/assets/ (Vite-Processed Assets)
Files inside src/assets/ can be imported directly into your TypeScript components. Vite processes them as build assets and generates cache-busting URLs; the separate ForgeWP Sharp pipeline may generate WebP variants for eligible raster files.
- Best for: Component-specific illustrations, background images, and UI graphics.
- Referencing in Code:
1import heroGraphic from "@/assets/hero-pattern.png"
2 import { WpImage } from "@forgewp/react"
3
4 export function Hero() {
5 return (
6 <section className="relative overflow-hidden py-20">
7 <WpImage
8 src={heroGraphic}
9 alt="Abstract decorative pattern"
10 className="absolute inset-0 -z-10 h-full w-full object-cover opacity-20"
11 />
12 </section>
13 )
14 }2. Automatic URL Rewriting (get_theme_file_uri)
In traditional WordPress PHP development, linking to any theme file requires wrapping every path in get_theme_file_uri():
1<img src="<?php echo esc_url( get_theme_file_uri( 'assets/logo.svg' ) ); ?>" />If you hardcode relative paths like <img src="./assets/logo.svg" />, the browser breaks on child routes (/blog/post-name/) because relative paths resolve against the current URL instead of the theme root.
ForgeWP eliminates this boilerplate across both <WpImage> and standard HTML tags:
1// Approach 1 (Recommended): Using <WpImage>
2<WpImage src="/assets/logo.svg" alt="ForgeWP" width={180} height={40} className="h-9 w-auto" />
3
4// Approach 2: Using standard <img> tag (Supported as a zero-overhead fallback for SVGs)
5<img src="/assets/logo.svg" alt="ForgeWP" className="h-9 w-auto" />At compile time (pnpm export), the compiler analyzes static asset paths across both <WpImage> and <img> tags and emits native WordPress PHP:
1<img src="<?php echo esc_url( get_theme_file_uri( 'assets/logo.svg' ) ); ?>" alt="ForgeWP" class="h-9 w-auto" />In local Vite development (pnpm dev), Vite serves the file instantly from disk with zero 404 errors.
Static download links under public/assets/ are also rewritten to the active theme URI:
1<a href="/assets/client-assets/preview.pdf">View preview</a>For an asset URL that comes from block attributes or other runtime data, use resolveWpAsset() so it resolves against the theme directory in WordPress while remaining a public-path URL in Vite:
1import { resolveWpAsset } from "@forgewp/react"
2
3<a href={resolveWpAsset(previewUrl)}>View preview</a>In generated block PHP, the compiler resolves this helper server-side as well, so the URL works before any client-side hydration.
3. Dynamic Runtime Asset Paths with useWpThemeUri()
The compiler automatically rewrites static paths inside JSX elements like <WpImage src="/assets/..." />. But what if you are building an interactive client component that needs to load an asset dynamically in JavaScript?
Examples include:
- Playing an audio notification with
new Audio() - Loading a 3D model or canvas texture in Three.js
- Dynamically requesting a theme JSON file via
fetch()
In the browser, client JavaScript cannot execute PHP's get_theme_file_uri(). ForgeWP provides the useWpThemeUri() hook from @forgewp/react:
1import { useWpThemeUri } from "@forgewp/react"
2
3export function AudioNotification() {
4 // Returns the active WordPress theme root URI
5 const themeUri = useWpThemeUri()
6
7 const handlePlaySound = () => {
8 // Dynamically construct asset path at runtime
9 const sound = new Audio(`${themeUri}/assets/sounds/success.mp3`)
10 sound.play()
11 }
12
13 return (
14 <button
15 onClick={handlePlaySound}
16 className="rounded-lg bg-primary px-4 py-2 font-semibold text-primary-foreground shadow"
17 >
18 Play Sound
19 </button>
20 )
21}How useWpThemeUri() Works
- In Local Development (
pnpm dev): Returns""(empty string) so${themeUri}/assets/...resolves cleanly to/assets/...from your local dev server. - In Production WordPress: ForgeWP's theme engine automatically exposes
window.forgeWpHydration.themeUrion pages with interactive islands. The hook reads this value, ensuring your dynamic scripts point to the live/wp-content/themes/my-themeURL without hardcoding domain names.
When to Use What: Summary
Use Case Recommended Tool Why? Visual images & graphics in JSX <WpImage src="..." />Compiler rewrites URL + adds intrinsic dimensions and preloading. Simple vector logos/SVGs in JSX <WpImage> or <img />Both are automatically rewritten to get_theme_file_uri() at build time.Dynamic JavaScript logic (Audio, Canvas, Fetch) useWpThemeUri()Provides the live theme root URL to browser scripts at runtime. |
|---|
4. Dynamic Icons with <WpIcon>
Icons are an essential part of modern web design. However, bundling an entire icon library (like Lucide or FontAwesome) in client JavaScript adds hundreds of kilobytes of unnecessary bloat.
ForgeWP includes <WpIcon>, an isomorphic primitive that dynamically renders vector glyphs:
1import { WpIcon } from "@forgewp/react"
2
3interface Feature {
4 title: string
5 description: string
6 iconName: string
7}
8
9export function FeatureCard({ title, description, iconName }: Feature) {
10 return (
11 <div className="rounded-xl border border-border bg-card p-6 shadow-sm">
12 <div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10 text-primary">
13 {/* Dynamic icon slug */}
14 <WpIcon name={iconName} provider="lucide" className="h-6 w-6" />
15 </div>
16 <h4 className="mb-1 text-lg font-bold text-foreground">{title}</h4>
17 <p className="text-sm text-muted-foreground">{description}</p>
18 </div>
19 )
20}Supported Icon Providers
You can specify the icon set via the provider prop:
Provider Description Examples provider="lucide" (Default)Uses Lucide React icons. name="star", name="map-pin", name="arrow-right"provider="dashicons"WordPress core administrative Dashicons. name="admin-site", name="dashboard", name="menu"provider="custom"Custom theme SVG sprite map. Custom SVG slugs configured in your theme |
|---|
How <WpIcon> Compiles in Production
- In Local Development: Loads the icon dynamically via ESM module resolution with a fallback placeholder if the icon name is mis-typed.
- In Production Theme Export: The compiler parses the icon name, extracts only the required SVG path data from the icon library, and bakes the raw vector SVG directly into the static PHP template.
- Client Overhead: 0 KB client JavaScript! Visitors download only pure inline SVG paths.
5. Best Practices for Vector SVGs in Themes
- Inline SVGs for Interactive Elements: If you need CSS hover color transitions or animations, import the SVG as a React component:
1export function TwitterIcon({ className }: { className?: string }) {
2 return (
3 <svg viewBox="0 0 24 24" fill="currentColor" className={className}>
4 <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
5 </svg>
6 )
7 }- Use
<WpImage>or<img>for Complex Illustrations: Heavy vector illustrations with hundreds of anchor points should be kept as standalone.svgfiles insrc/assets/to avoid bloating your JavaScript bundle. - Always Include
aria-hiddenon Decorative Icons: If an icon is accompanied by text, addaria-hidden="true"so screen readers do not redundantly announce it.
Common Questions & FAQs
Can I use standard `<img>` tags instead of `<WpImage>` for local theme images?
Yes. Both <WpImage src="/assets/..." /> and <img src="/assets/..." /> are rewritten to use the theme file URI. The Sharp pipeline can generate WebP variants for eligible raster assets, but the current compiler does not attach those variants as srcset to every static image. WordPress attachments rendered with wp_get_attachment_image() use WordPress's own responsive sizes.
What should the WordPress theme screenshot be?
WordPress requires a screenshot.png in the theme root to display your theme preview in Appearance → Themes. Save a 1200 × 900 px PNG file in your project's public/screenshot.png. ForgeWP automatically copies it to the theme root during pnpm export.
Next Step: The Sharp Image Optimization Pipeline
Proceed to The Sharp Image Optimization Pipeline to learn how ForgeWP automates WebP conversions and preloads critical hero media during export!