theme-forge
v1.0.0
Published
A powerful, database-free visual content editor for any frontend project. Scans source code with AST parsing to extract every text string and image, lets you edit them in a modern web UI, writes changes back to the exact source location, and rebuilds the
Maintainers
Readme
🔨 Theme Forge
An embeddable, database-free content/theme editor for any frontend project.
Install it into your app and get a full visual "Theme Customizer" inside your own admin panel. Click any text or image in a live preview and edit it in place, change theme colors, upload images, then save back to source and rebuild — no database required. Your source code stays the single source of truth.
- Visual / WYSIWYG editor — click text to edit inline, drag-and-drop images, pick colors, with undo/redo and keyboard shortcuts.
- AST-based (Babel) — understands JSX/TSX and edits only real text, string attributes, image paths and colors, never your logic.
- Offset-precise, auto-escaping writes straight back to your source files (correct escaping for JSX text, JSX attributes and JS/TS string literals).
- Image uploads saved into your
public/folder + an image library browser. - Live rebuild via a child process, streamed to a build console (SSE).
- Ships as two drop-in pieces: a Next.js API handler and a React panel.
Built for React / Next.js (App Router). Works on any project whose content lives in
.js / .jsx / .ts / .tsxfiles.
Table of contents
- Requirements
- Install
- Quick start (Next.js App Router)
- Full configuration reference
<ThemeForgePanel />props- Using the visual editor
- Per-theme / per-page preview
- Production: make changes appear without a manual restart
- What gets detected
- CLI helpers
- Programmatic API
- Troubleshooting
- How it works
- License
Requirements
- A Next.js app using the App Router (
app/directory). - React 18+ and react-dom 18+ (peer dependencies — you already have them).
- Node.js 18+.
- Your editable content lives in
.js / .jsx / .ts / .tsxfiles.
Not on Next.js? You can still use the programmatic API and the CLI to scan, edit and rebuild from your own server/scripts.
Install
npm install theme-forge
# or
pnpm add theme-forge
# or
yarn add theme-forgeQuick start (Next.js App Router)
Three small steps. After this you have a working customizer page in your admin.
Step 1 — Mount the API handlers
Create a catch-all route. The folder name [...tf] matters (it captures the
sub-actions like scan, save, upload, images, build).
app/api/theme-forge/[...tf]/route.ts
import { createThemeForgeHandlers } from "theme-forge/next";
export const { GET, POST } = createThemeForgeHandlers({
root: "src", // source dir to scan (use "." if no src/)
publicDir: "public", // your static folder
imageUploadDir: "public/uploads",
buildCommand: "npm run build", // run on "Save & Rebuild" (use your package manager)
});
// Theme Forge reads/writes files and spawns a build — keep it on the Node runtime.
export const dynamic = "force-dynamic";
export const runtime = "nodejs";Step 2 — Externalize the Node-only dependencies
Theme Forge uses Babel + fast-glob on the server. Tell Next.js not to bundle
them so the API route runs them natively.
next.config.ts (or next.config.js)
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: [
"@babel/parser",
"@babel/traverse",
"@babel/types",
"fast-glob",
],
};
export default nextConfig;⚠️ Do not put
theme-forgeitself inserverExternalPackages— it contains a React client component and externalizing it causes a duplicate-React (useContextis null) error. Only externalize the four packages above.
Step 3 — Drop the panel into an admin page
app/admin/theme-customizer/page.tsx
"use client";
import { ThemeForgePanel } from "theme-forge/react";
export default function Page() {
return <ThemeForgePanel basePath="/api/theme-forge" />;
}That's it. Open /admin/theme-customizer, click text/images in the live preview,
edit them, then hit Save changes or Save & Rebuild.
The panel is fully self-styled (scoped CSS) — it looks correct with no Tailwind configuration required.
Full configuration reference
Pass options to createThemeForgeHandlers({...}), or create a
theme-forge.config.json in your project root (npx theme-forge init). Inline
options override the config file.
{
"root": "src",
"include": ["**/*.{js,jsx,ts,tsx}"],
"exclude": ["**/node_modules/**", "**/.next/**", "**/*.test.*"],
"textAttributes": ["alt", "title", "placeholder", "label", "aria-label", "content", "value"],
"imageAttributes": ["src", "poster", "srcSet"],
"publicDir": "public",
"imageUploadDir": "public/uploads",
"publicUrlBase": "/",
"buildCommand": "npm run build",
"minTextLength": 1,
"skipNonAlphaText": true
}| Option | Default | Description |
| ------------------ | ---------------------------------------- | -------------------------------------------------------- |
| root | "src" | Source directory to scan (relative to project root). |
| include | ["**/*.{js,jsx,ts,tsx}"] | Glob(s) of files to scan. |
| exclude | node_modules / .next / dist / tests … | Glob(s) to ignore. |
| textAttributes | alt, title, placeholder, … | JSX attributes treated as editable text. |
| imageAttributes | src, poster, srcSet | JSX attributes treated as images. |
| publicDir | "public" | Static dir that serves images (Next.js: public). |
| imageUploadDir | "public/uploads" | Where uploaded images are written. |
| publicUrlBase | "/" | URL prefix that maps to publicDir. |
| buildCommand | "npm run build" | Command run by Save & Rebuild. |
| restartCommand | "" | Optional command run (detached) after a successful build to restart the server in production. See Production. |
| minTextLength | 1 | Ignore text shorter than this. |
| skipNonAlphaText | true | Skip strings with no letters (numbers/symbols only). |
Project without a
src/folder? Setroot: "."and add anexcludeforapp/api/**if you don't want the API route itself scanned.
<ThemeForgePanel /> props
| Prop | Default | Description |
| ------------------ | -------------------- | ---------------------------------------------------- |
| basePath | "/api/theme-forge" | Where the API handlers are mounted. |
| initialPreviewUrl| "/" | Page URL loaded in the live preview iframe. |
| hideHeader | false | Hide the built-in heading (use your own page title). |
| className | — | Extra class on the outer wrapper. |
basePath must match where you mounted the route in Step 1.
Using the visual editor
- Edit text — click any text in the preview, type, press Enter (or click away) to apply. Esc cancels.
- Replace an image — click an image to open its panel (upload a file, pick one from the image library, or paste a URL), or drag-and-drop an image file straight onto it.
- Theme colors — open the colors panel to edit detected color tokens with a color picker.
- Devices — preview at desktop / tablet / mobile widths.
- List view — toggle to a searchable/filterable list of every item.
Keyboard shortcuts
| Shortcut | Action |
| ------------------------------ | --------------- |
| Ctrl/Cmd + S | Save changes |
| Ctrl/Cmd + Z | Undo |
| Ctrl/Cmd + Shift + Z / + Y | Redo |
| Ctrl/Cmd + Shift + B | Save & Rebuild |
Per-theme / per-page preview
To preview/edit a specific theme or route, point the panel at any URL via
initialPreviewUrl. For example, drive it from a query param so a "Customize"
button on a theme card can deep-link into the editor:
"use client";
import { Suspense } from "react";
import { useSearchParams } from "next/navigation";
import { ThemeForgePanel } from "theme-forge/react";
function Customizer() {
const theme = useSearchParams().get("theme");
const previewUrl = theme ? `/?theme=${encodeURIComponent(theme)}` : "/";
return <ThemeForgePanel basePath="/api/theme-forge" initialPreviewUrl={previewUrl} hideHeader />;
}
export default function Page() {
// useSearchParams must be wrapped in <Suspense> in the App Router.
return (
<Suspense fallback={null}>
<Customizer />
</Suspense>
);
}Then link to /admin/theme-customizer?theme=my-theme from anywhere.
Production: make changes appear without a manual restart
In development (next dev) edits show up immediately. In production you run
a long-lived server (next start). next build writes a new .next build, but
the already-running next start process keeps serving the old build until it is
restarted — so your saved changes won't appear until you restart the server.
Theme Forge solves this with restartCommand: after a successful build it runs
that command detached (so it survives the server being torn down), letting your
process manager bring the server back up on the fresh build.
Run your server under a process manager and point restartCommand at it:
pm2
// app/api/theme-forge/[...tf]/route.ts
export const { GET, POST } = createThemeForgeHandlers({
root: "src",
buildCommand: "npm run build",
restartCommand: "pm2 restart my-app", // <-- restarts after build
});pm2 start "npm run start" --name my-appsystemd
restartCommand: "sudo systemctl restart my-app"Docker / Kubernetes — trigger a rolling restart, e.g.
restartCommand: "kubectl rollout restart deployment/my-app", or restart the
container via your orchestrator.
Without a process manager, a bare
next startthat is killed will simply exit and stay down. Always run production behind pm2 / systemd / a container so the restart brings it back automatically. LeaverestartCommandempty ("") to disable auto-restart and restart manually.
What gets detected
Theme Forge parses the AST and extracts editable items as one of three kinds:
- text — JSX text between tags, plus strings in data structures (object properties, arrays, JSX expressions) that look like human-readable copy.
- image — string values that resolve to an image (file extensions like
.png/.jpg/.webp/.svg,/images|/assets|/uploads/…paths,data:image/…, remote image URLs), in attributes or in data. - color —
#hex,rgb()/rgba(),hsl()/hsla()literals.
To avoid breaking your app it skips: imports, type/enum literals, strings used
in comparisons/logic (e.g. if (x === "foo")), and known non-text keys
(href, className, id, slug, config keys, etc.). Each item is written back
with the correct escaping for its location, so quotes, <, >, {, } and
newlines never corrupt your source.
CLI helpers
npx theme-forge scan # print a summary of editable content
npx theme-forge build # run the build command with live output
npx theme-forge init # write a starter theme-forge.config.jsonProgrammatic API
Use the core directly from your own server or scripts (no Next.js required):
import { loadConfig, scanProject, applyEdits, Builder } from "theme-forge";
const config = loadConfig(process.cwd(), { root: "src" });
// 1) Scan
const { items } = await scanProject(config);
// 2) Edit (by stable id)
applyEdits(config, [{ id: items[0].id, value: "New text" }]);
// 3) Rebuild
const builder = new Builder(config);
builder.on("event", (e) => console.log(e));
builder.start();Also exported: extractFromCode, isColor, isImageSource, listImages,
saveUpload, and types (ContentItem, ResolvedConfig, …).
Troubleshooting
Module not found: Can't resolve 'theme-forge/next' (Turbopack with a local
symlink) — install the packed tarball instead of a file: link so it is physically
copied into node_modules:
cd theme-forge && npm run build && npm pack
cd ../your-app && pnpm add ../theme-forge/theme-forge-1.0.0.tgzTypeError: Cannot read properties of null (reading 'useContext') — you put
theme-forge into serverExternalPackages. Remove it; only externalize
@babel/* and fast-glob (see Step 2).
"use client" / hooks errors — make sure the page that renders
<ThemeForgePanel /> is itself a client component ("use client" at the top) or
imported into one.
Build doesn't start / wrong command — set buildCommand to match your package
manager (pnpm build, yarn build, npm run build).
Some text isn't clickable in the preview — text that renders differently from
source (e.g. HTML entities like ') may not match in the visual layer; edit
those from the List view instead.
How it works
source (.tsx) ─▶ Babel AST ─▶ ContentItem[] (text / image / color, with offsets)
│
<ThemeForgePanel/> (edit values in your admin)
│ /api/theme-forge/*
save ─▶ re-parse file ─▶ match by id ─▶ escape ─▶ splice offsets ─▶ write
│
rebuild ─▶ child_process spawn(buildCommand) ─▶ live SSE log streamNo database, no lock-in. Your code stays the single source of truth.
License
MIT
