here’s the complete batch-import package so your team can load dozens (hundreds) of DOCX plans in one go, keep the manifest synced, and validate everything — all styled to your brand (green buttons, orange Download).

I’m giving you three ways to import (use any/all):

CLI scanner (auto-index a folder of DOCX + previews)

CSV importer (API + Admin UI) for bulk metadata

Sidecar .meta.json (per-file overrides)

0) Folder layout (source of truth)
public/
  templates/business-plan/
    docs/        # .docx files (exact filenames preserved)
    previews/    # {id}.jpg|png
  site/data/
    manifest.bp.json   # gets created/updated

1) Shared helpers (tokens already provided)

/src/lib/bp-io.ts

import path from "path";
import fs from "fs";

export type BPItem = {
  id: string;
  name: string;
  category?: string;
  tags?: string[];
  previewUrl: string;
  docxUrl: string;
  updatedAt?: string;
  sections?: string[];
};

export type BPManifest = {
  collection: "business-plan";
  version: number;
  items: BPItem[];
};

export const PUBLIC_DIR = path.join(process.cwd(), "public");
export const DOC_DIR = path.join(PUBLIC_DIR, "templates", "business-plan", "docs");
export const PREV_DIR = path.join(PUBLIC_DIR, "templates", "business-plan", "previews");
export const DATA_DIR = path.join(PUBLIC_DIR, "site", "data");
export const MANIFEST = path.join(DATA_DIR, "manifest.bp.json");

export function ensureDirs() {
  [DOC_DIR, PREV_DIR, DATA_DIR].forEach(d => { if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); });
}

export function loadManifest(): BPManifest {
  ensureDirs();
  if (!fs.existsSync(MANIFEST)) {
    const fresh: BPManifest = { collection: "business-plan", version: 1, items: [] };
    fs.writeFileSync(MANIFEST, JSON.stringify(fresh, null, 2), "utf8");
    return fresh;
  }
  const raw = fs.readFileSync(MANIFEST, "utf8");
  try {
    const json = JSON.parse(raw);
    json.collection = "business-plan";
    json.version = Number(json.version || 1);
    json.items = Array.isArray(json.items) ? json.items : [];
    return json as BPManifest;
  } catch {
    return { collection: "business-plan", version: 1, items: [] };
  }
}

export function saveManifest(m: BPManifest) {
  m.collection = "business-plan";
  m.version = Number(m.version || 1);
  fs.writeFileSync(MANIFEST, JSON.stringify(m, null, 2), "utf8");
}

export const kebab = (s: string) =>
  (s || "").toLowerCase().trim().replace(/[^a-z0-9\-]/g, "-").replace(/\-+/g, "-");

export function upsert(items: BPItem[], item: BPItem): BPItem[] {
  const idx = items.findIndex(x => x.id === item.id);
  if (idx >= 0) items[idx] = item; else items.push(item);
  return items;
}

2) CLI Scanner (auto-build manifest from files)

Derives id from preview filename {id}.jpg/png

Matches a .docx (by any name) where {id} appears in the filename or uses first .docx in folder if exactly one

Reads optional overrides from previews/{id}.meta.json (see §4)

/scripts/build-bp-manifest.ts

#!/usr/bin/env ts-node
import fs from "fs";
import path from "path";
import { DOC_DIR, PREV_DIR, loadManifest, saveManifest, upsert, BPItem } from "../src/lib/bp-io";

const today = new Date().toISOString().slice(0,10);

function titleCaseFromId(id: string) {
  return id.replace(/\-+/g," ").replace(/\b\w/g, c => c.toUpperCase());
}

function findDocForId(id: string): string | null {
  const docs = fs.readdirSync(DOC_DIR).filter(f => /\.docx$/i.test(f));
  // Prefer filename that contains the id
  const hit = docs.find(f => f.toLowerCase().includes(id));
  return hit || docs.find(() => true) || null;
}

(async () => {
  const manifest = loadManifest();

  const previews = fs.readdirSync(PREV_DIR).filter(f => /\.(png|jpg|jpeg)$/i.test(f));
  let added = 0;

  for (const prev of previews) {
    const id = prev.replace(/\.(png|jpe?g)$/i, "").toLowerCase();
    const docName = findDocForId(id);
    if (!docName) { console.warn(`No DOCX found for id=${id} (preview=${prev})`); continue; }

    // Defaults
    let item: BPItem = {
      id,
      name: `Business Plan — ${titleCaseFromId(id)}`,
      category: "General",
      tags: [],
      previewUrl: `/templates/business-plan/previews/${prev}`,
      docxUrl: `/templates/business-plan/docs/${docName}`,
      updatedAt: today,
      sections: [
        "Executive Summary","Market Analysis","Products & Services",
        "Marketing & Sales","Operations Plan","Organization & Management","Financial Plan","Appendices"
      ]
    };

    // Optional sidecar
    const metaPath = path.join(PREV_DIR, `${id}.meta.json`);
    if (fs.existsSync(metaPath)) {
      try {
        const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
        item = { ...item, ...meta, id }; // never override ID
      } catch (e) {
        console.warn(`Invalid meta JSON for ${id}:`, (e as any).message);
      }
    }

    upsert(manifest.items, item);
    added++;
  }

  saveManifest(manifest);
  console.log(`✓ Manifest updated. Items: ${manifest.items.length} (processed ${added} previews).`);
})();


Run:

# once
npm i -D ts-node @types/node

# then
ts-node scripts/build-bp-manifest.ts

3) CSV Import (API + Admin UI)
3a) API route (Express)

/server/routes/bpImportCsv.ts

import { Router } from "express";
import multer from "multer";
import { parse } from "csv-parse/sync";
import { loadManifest, saveManifest, upsert, kebab, BPItem } from "../../src/lib/bp-io";

const upload = multer();
const router = Router();

router.post("/import-csv", upload.single("csv"), (req, res) => {
  try {
    if (!req.file) return res.status(400).json({ error: "CSV file required" });
    const csv = req.file.buffer.toString("utf8");
    const rows: any[] = parse(csv, { columns: true, skip_empty_lines: true, trim: true });

    const manifest = loadManifest();
    const today = new Date().toISOString().slice(0,10);

    let count = 0;
    for (const r of rows) {
      const id = kebab(r.id || r.name);
      if (!id) continue;
      const item: BPItem = {
        id,
        name: r.name || `Business Plan — ${id}`,
        category: r.category || "General",
        tags: (r.tags || "").split("|").map((t: string)=>t.trim()).filter(Boolean),
        previewUrl: r.previewUrl,
        docxUrl: r.docxUrl,
        updatedAt: r.updatedAt || today,
        sections: (r.sections || "").split("|").map((t: string)=>t.trim()).filter(Boolean),
      };
      if (!item.previewUrl || !item.docxUrl) continue;
      upsert(manifest.items, item);
      count++;
    }

    saveManifest(manifest);
    res.json({ ok: true, imported: count, total: manifest.items.length });
  } catch (e:any) {
    console.error(e);
    res.status(500).json({ error: e?.message || "Server error" });
  }
});

export default router;


Mount it:

import bpImportCsv from "./routes/bpImportCsv";
app.use("/api/bp-templates", bpImportCsv);

3b) Admin UI to upload CSV (green buttons)

/src/AdminImportBusinessPlans.tsx

import React, { useRef, useState } from "react";
import { DashboardTemplatePage } from "./DashboardTemplatePage";
import { COLORS, btnBase } from "./ui-tokens";

export default function AdminImportBusinessPlans() {
  const [file, setFile] = useState<File | null>(null);
  const [msg, setMsg] = useState<string>("");
  const [busy, setBusy] = useState(false);
  const ref = useRef<HTMLInputElement>(null);

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!file) return setMsg("Select a CSV file first.");
    setMsg("");
    try {
      setBusy(true);
      const fd = new FormData();
      fd.append("csv", file);
      const res = await fetch("/api/bp-templates/import-csv", { method: "POST", body: fd });
      const json = await res.json().catch(()=>({}));
      if (!res.ok) throw new Error(json?.error || `HTTP ${res.status}`);
      setMsg(`Imported ${json.imported} items. Library now ${json.total}. ✅`);
    } catch (e:any) {
      setMsg("Import failed: " + (e?.message || "Unknown error"));
    } finally {
      setBusy(false);
    }
  };

  return (
    <DashboardTemplatePage title="Import Business Plans (CSV)">
      <form onSubmit={submit} className="space-y-4">
        <div className="rounded-xl border-dashed border-2 p-4 flex items-center justify-between">
          <div className="text-sm">
            {file ? <span className="font-medium">{file.name}</span> : "Click Choose to select a .csv"}
            <div className="text-[11px] text-gray-500">Columns: id,name,category,tags,previewUrl,docxUrl,updatedAt,sections</div>
          </div>
          <div className="flex gap-2">
            <button type="button" className={btnBase} style={{ backgroundColor: COLORS.green, color: COLORS.white }} onClick={()=>ref.current?.click()}>Choose</button>
            {file && <button type="button" className={btnBase} style={{ backgroundColor: COLORS.darkGray, color: COLORS.white }} onClick={()=>setFile(null)}>Remove</button>}
          </div>
        </div>
        <input ref={ref} type="file" accept=".csv" className="hidden" onChange={(e)=>setFile(e.target.files?.[0] || null)} />
        <button className={btnBase} style={{ backgroundColor: COLORS.green, color: COLORS.white }} disabled={busy} type="submit">
          {busy ? "Importing…" : "Import CSV"}
        </button>
        {msg && <p className="text-sm text-gray-600">{msg}</p>}
        <a
          className={btnBase}
          style={{ backgroundColor: COLORS.orange, color: COLORS.white }}
          href="/site/samples/bp-import-sample.csv"
          download
        >
          Download Sample CSV
        </a>
      </form>
    </DashboardTemplatePage>
  );
}


Route + Sidebar

// AppRoutes.tsx
import AdminImportBusinessPlans from "./AdminImportBusinessPlans";
<Route path="/admin/import-bp-templates" element={<AdminImportBusinessPlans />} />

// Sidebar
<NavLink to="/admin/import-bp-templates" className={({isActive})=>`${link} ${isActive?active:""}`}>
  <span>Import BP (CSV)</span>
</NavLink>

3c) Sample CSV (place for download)

/public/site/samples/bp-import-sample.csv

id,name,category,tags,previewUrl,docxUrl,updatedAt,sections
barber-shop,Business Plan — Barber Shop,Personal Services,services|retail|local,/templates/business-plan/previews/barber-shop.jpg,/templates/business-plan/docs/Business_Plan_Barber_Shop_White_Label_GeneralSections.docx,2025-09-26,Executive Summary|Market Analysis|Products & Services|Marketing & Sales|Operations Plan|Organization & Management|Financial Plan|Appendices
daycare,Business Plan — Daycare,Childcare,education|services,/templates/business-plan/previews/daycare.jpg,/templates/business-plan/docs/Business_Plan_Daycare_White_Label_GeneralSections.docx,2025-09-26,Executive Summary|Market Analysis|Products & Services|Marketing & Sales|Operations Plan|Organization & Management|Financial Plan|Appendices

4) Optional sidecar metadata per template

Put a JSON next to the preview to override defaults:

/public/templates/business-plan/previews/barber-shop.meta.json

{
  "name": "Business Plan — Barber Shop",
  "category": "Personal Services",
  "tags": ["services","retail","local"],
  "sections": [
    "Executive Summary","At a Glance","Company Overview","Products & Services",
    "Market Analysis","Marketing & Sales","Operations Plan","Organization & Management",
    "Financial Plan","Appendices"
  ]
}


Then run the CLI scanner — it merges these values when creating/updating the manifest.

5) QA / Validator (already provided)

Use the Validate page to HEAD-check all previewUrl and docxUrl after imports. If you want a one-liner sanity check in CI:

/scripts/check-bp-manifest.ts

#!/usr/bin/env ts-node
import fs from "fs";
import { loadManifest } from "../src/lib/bp-io";
const m = loadManifest();
let ok = true;
for (const it of m.items) {
  if (!it.previewUrl || !it.docxUrl) { ok = false; console.error("Missing URL:", it.id); }
}
console.log(`Items: ${m.items.length} — ${ok ? "OK" : "Issues found"}`);
process.exit(ok ? 0 : 1);

6) UX details (brand-aligned)

Download buttons: orange #FF8800 / white text (already in gallery)

All other CTA buttons: green #00C851 / white text

Cards: white, soft border, subtle hover shadow

Chips/tags: light gray pills

How to use (happy path)

Drop your DOCX files into public/templates/business-plan/docs/.

Drop previews named {id}.jpg|png into public/templates/business-plan/previews/.

(Optional) Add {id}.meta.json for overrides.

Run the CLI:

ts-node scripts/build-bp-manifest.ts


—or— upload a CSV via Admin → Import BP (CSV).

Visit Admin → Validate BP Manifest to confirm everything is reachable.

Your public gallery at Business Plan Templates auto-lists the items with Download (orange) and Open in Google Docs (green).