-
Notifications
You must be signed in to change notification settings - Fork 0
Building a Ware
A ware is any web app packaged as a .wp file (a renamed ZIP archive) with a manifest.json. Once uploaded to Bazaar, it appears as a full-screen menu page in wp-admin — completely isolated from WordPress's own styles and scripts.
- The Mental Model
- Anatomy of a Ware
- Quickstart: Vanilla JS
- Building with a Framework
- Shared Libraries
- Communicating with WordPress
- Using @bazaar/client
- iframe Sandbox Capabilities
- Updating a Ware
- Debugging Tips
Bazaar makes the admin a platform where anyone can contribute an app without understanding WordPress internals.
Your ware is just a website. It runs in a sandboxed <iframe> inside wp-admin. You have full control over HTML, CSS, and JS. The only WordPress knowledge you need is writing manifest.json.
flowchart LR
A["Your app\n(React, Vue, vanilla…)"] --> B["manifest.json"]
B --> C[".wp file\n(renamed ZIP)"]
C --> D["Bazaar upload"]
D --> E["Sidebar menu page"]
my-ware.wp ← renamed .zip
├── manifest.json ← REQUIRED — ware metadata
├── index.html ← REQUIRED — entry point
├── icon.svg ← optional sidebar icon (20×20 recommended)
└── assets/
├── app.js
├── app.css
└── logo.png
Important
manifest.json must exist at the root level of the ZIP — not inside a subdirectory. The same applies to the entry HTML file unless you set a path in manifest.json.
The fastest possible ware — no build step, no dependencies, no tooling.
1. manifest.json
{
"name": "Hello Ware",
"slug": "hello-ware",
"version": "1.0.0",
"author": "Your Name",
"entry": "index.html",
"menu": {
"title": "Hello",
"position": 99
}
}2. index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello Ware</title>
<style>
body {
font-family: system-ui;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
h1 { color: #2271b1; }
</style>
</head>
<body>
<h1>Hello from Bazaar!</h1>
</body>
</html>3. Package and upload
zip hello-ware.wp manifest.json index.html
# Upload in the Bazaar admin page"Hello" now appears in your sidebar. That's the entire workflow.
Wares work with any framework that produces static output.
Tip
The fastest way to start is npm create ware@latest — it scaffolds a complete project with the right Vite config, manifest.json, and a package script that outputs a .wp file. The React and Vue scaffolds include shared library optimisation out of the box (see Shared Libraries below).
React (Vite) — recommended approach
Use the Bazaar scaffold for the best out-of-the-box experience:
npm create ware@latest # choose React when promptedOr manually with shared-library support:
npm create vite@latest my-ware -- --template react
cd my-ware && npm installmanifest.json:
{
"name": "My Ware",
"slug": "my-ware",
"version": "1.0.0",
"entry": "index.html",
"menu": { "title": "My Ware" },
"shared": ["react", "react-dom", "react/jsx-runtime"]
}vite.config.ts:
build: {
rollupOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'],
},
},npm run build
cp manifest.json dist/
cd dist && zip -r ../../my-ware.wp . && cd ../..Vue — recommended approach
npm create ware@latest # choose Vue when promptedOr manually with shared-library support:
npm create vue@latest my-ware
cd my-ware && npm installmanifest.json:
{
"name": "My Ware",
"slug": "my-ware",
"version": "1.0.0",
"entry": "index.html",
"menu": { "title": "My Ware" },
"shared": ["vue"]
}vite.config.ts:
build: {
rollupOptions: {
external: ['vue'],
},
},npm run build
cp manifest.json dist/
cd dist && zip -r ../../my-ware.wp . && cd ../..Svelte / SvelteKit (static adapter)
npx sv create my-ware
cd my-ware && npm install && npm run build
cp manifest.json build/
cd build && zip -r ../../my-ware.wp . && cd ../..Svelte compiles to vanilla JS so no shared library declaration is needed.
When multiple wares use the same framework (React, Vue), each iframe would normally download and parse its own copy. Bazaar solves this with a two-layer approach:
Layer 1 — Import Maps: The shell hosts versioned, content-hashed bundles of React and Vue. Wares that declare "shared": ["react", "react-dom"] in their manifest.json have an <importmap> injected into their HTML, pointing those package names at the shared URLs. The browser downloads React once; every subsequent ware iframe loads the already-compiled bytecode from the V8 cache.
Layer 2 — Service Worker Cache: All ware assets under wp-content/bazaar/ (and the shared bundles themselves) are cached by the Bazaar service worker after the first fetch. On subsequent page loads, nothing hits the network — not even a conditional request.
Result:
| Scenario | Without shared libs | With shared libs |
|---|---|---|
| 3 React wares, first load | 3 × ~140 KB download + parse | 1 × ~140 KB download + cached bytecode |
| Any ware, repeat visit | ETag round-trip per asset | 0 network requests (SW cache) |
Opt in by adding "shared" to your manifest.json and marking the packages as external in your Vite config. The create-ware React and Vue scaffolds do both automatically.
Save yourself the manual steps by adding a package script to your package.json:
{
"scripts": {
"build": "vite build",
"package": "npm run build && cp manifest.json dist/ && cd dist && zip -r ../../$(node -p \"require('../manifest.json').slug\").wp . && cd .."
}
}npm run package # builds + zips in one stepYour ware runs in a sandboxed iframe on the same origin as WordPress (served via the Bazaar REST endpoint). That means you can make authenticated requests to any WordPress REST API endpoint.
The iframe src URL includes a _wpnonce query parameter. Read it from the URL:
const nonce = new URLSearchParams( window.location.search ).get( '_wpnonce' );const nonce = new URLSearchParams( window.location.search ).get( '_wpnonce' );
// Fetch posts
const posts = await fetch( '/wp-json/wp/v2/posts', {
headers: { 'X-WP-Nonce': nonce },
} ).then( r => r.json() );
// Get current user
const me = await fetch( '/wp-json/wp/v2/users/me', {
headers: { 'X-WP-Nonce': nonce },
} ).then( r => r.json() );await fetch( '/wp-json/wp/v2/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce,
},
body: JSON.stringify({ title: 'My Post', status: 'publish' }),
} );Tip
Don't hardcode /wp-json/. WordPress can be installed in a subdirectory. Extract the base URL from the iframe's src instead:
// The iframe src is: https://example.com/wp-json/bazaar/v1/serve/my-ware/index.html
const wpRoot = window.location.href.split( '/wp-json/' )[ 0 ];
const restBase = wpRoot + '/wp-json';Bazaar applies one of three sandbox policies depending on the ware's trust level:
standard (default)
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-downloads"trusted — adds allow-modals (permits alert() / confirm())
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-downloads allow-modals"verified — adds allow-modals and allow-popups-to-escape-sandbox
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-downloads allow-popups-to-escape-sandbox allow-modals"| Permission | What It Enables |
|---|---|
allow-scripts |
Run JavaScript |
allow-forms |
Submit HTML forms |
allow-same-origin |
Make authenticated fetch requests to WordPress |
allow-popups |
Open links in new tabs |
allow-downloads |
Trigger file downloads |
allow-modals |
alert() / confirm() (trusted and verified only) |
allow-popups-to-escape-sandbox |
Links that open a full page context (verified only) |
The trust level is set by the admin and stored in the ware registry, not declared in the manifest. allow-top-navigation is excluded at all trust levels. Wares cannot redirect the parent page.
To upgrade an installed ware, use the --force flag:
wp bazaar install my-ware-v2.wp --forceOr from the admin UI: delete the old ware, then upload the new one.
Note
A GUI update flow with version comparison is on the roadmap.
- DevTools work normally. The ware iframe is fully inspectable — set breakpoints, inspect network requests, and read console output as you would any web app.
-
HTTP status codes are meaningful.
401= not logged in.403= insufficient capability or ware is disabled.404= file path wrong. -
Inspect the registry with
wp bazaar info <slug>to see exactly what Bazaar has stored. -
Disable asset caching in development by setting
SCRIPT_DEBUG=trueandWP_DEBUG=trueinwp-config.php(or use.wp-env.json). -
Tail the debug log with
wp eval 'echo WP_CONTENT_DIR;'to find the path, thentail -f <path>/debug.log.
@bazaar/client is a TypeScript library that handles WordPress plumbing so you can focus on your app.
npm install @bazaar/clientFramework-agnostic core:
import { getBazaarContext, wpJson } from '@bazaar/client';
const ctx = getBazaarContext();
// { nonce, restUrl, serveUrl, slug, adminColor }
// Authenticated fetch — nonce added automatically
const posts = await wpJson('/wp/v2/posts?per_page=5');React hooks:
import { useCurrentUser, useWpPosts } from '@bazaar/client/react';
function App() {
const { user, loading: userLoading } = useCurrentUser();
const { posts, loading } = useWpPosts({ per_page: 10 });
if (loading || userLoading) return <p>Loading…</p>;
return <div>{posts.map(p => <h2 key={p.id}>{p.title.rendered}</h2>)}</div>;
}See the packages/client README for the full API reference.
-
Manifest Reference — every
manifest.jsonfield explained - REST API — full endpoint docs and ware-to-WordPress patterns
- WP-CLI — install and manage wares from the terminal
- Architecture — how shared libraries and the service worker cache work
Getting Started
Reference
Internals