# DEVELOPER SPEC v2 — Per-Product Content Template + Product Manager (Next.js + NextJS Editor plugin)

> Audience: the implementing developer/agent (Gemini). Written by the lead (Claude).
> **This v2 replaces v1 completely.** Follow it literally. If unclear, STOP and ask the owner. Never guess.

---

## 0. What we are building (3 parts)

1. **Template** (optional, per product): JSON/TXT/HTML upload that replaces the product page's **short** and **long** description areas and adds extra tabs.
2. **Product list + Edit modal** in the plugin: edit name, price, category, brand and the template. Saving writes to **WooCommerce** (the original data).
3. **Add New Product** in the plugin: create a WooCommerce product from the plugin (name, price, category, brand, optional template import).

---

## 1. Hard rules (non-negotiable)

1. **No template = the page is EXACTLY as it is today.** Every current element stays: short text under price, Description & Highlights, Key Ingredients, How to Use, Delivery & Exchange, trust badges, concern box, buy buttons, related products. Do not touch that code path.
2. **Template present = only the short area and the long area change** (details in §8). Everything else on the page (price, buttons, trust badges, concerns box, related products, sticky bar) is unchanged.
3. **Name, price, size, category, brand ALWAYS come from WordPress/WooCommerce**, on every page view, with or without a template. The template NEVER supplies display values for these.
4. **Delivery & returns:** global plugin text (`product_page.delivery_text`) is used as today. Template may override it only if it includes `delivery_returns`.
5. Template is **per single product**, never global.
6. Desktop and mobile show the **same order/UI**.
7. Plugin UI must be **clean**: labels, buttons, icons only. No hint paragraphs or filler text.
8. Do not break other plugin tabs/settings or rename existing option keys.
9. Bump plugin version; rebuild both zips with the existing scripts only after all tests pass.
10. Never invent data. Never auto-fill missing sections.

---

## 2. Files you will touch (verify paths)

| Area | File |
|---|---|
| Plugin PHP | `wp-plugin/nextjs-editor/nextjs-editor.php` (large; add one clearly delimited new block, do not reformat existing code) |
| Plugin JS / CSS | `wp-plugin/nextjs-editor/admin-script.js`, `admin-style.css` |
| Product type | `src/types/woocommerce.ts` (add optional `template`) |
| Product mapping | `src/lib/wpgraphql.ts` (`fetchFromWooCommerceRest`, mapping block returning `{ id, name, slug, brand, price, ... }`; brand logic is `extractBrand()`) |
| Product page | `src/app/product/[slug]/page.tsx`, `ProductDetailClient.tsx` |
| Styles | `src/app/globals.css` (verify `.acc .ing .howto .who .spec .full-ing`; copy from demo if missing) |
| Reference UI | `ui demo/roseebelle-app (9).html` -> `pageProduct()` (lines ~1427–1475). Match markup/behavior. |

---

## 3. Template data model (JSON is canonical; TXT/HTML are parsed into it)

```json
{
  "template_version": 1,
  "lead": "1-2 sentence short description.",
  "benefits": ["Max 4 shown"],
  "description": ["Paragraph 1", "Paragraph 2"],
  "key_ingredients": [ { "name": "Niacinamide 5%", "text": "What it does" } ],
  "how_to_use": ["Step 1", "Step 2"],
  "who_is_it_for": { "lead": "Suitable for all skin types.", "points": ["Dark spots"] },
  "product_details": [ { "label": "Skin type", "value": "All skin types" } ],
  "storage": "Store in a cool, dry place.",
  "full_ingredients": "Water, Niacinamide, ...",
  "delivery_returns": "OPTIONAL",
  "product": {
    "name": "AXIS-Y Dark Spot Correcting Glow Serum",
    "regular_price": 2000,
    "sale_price": 1170,
    "size": "50ml",
    "brand": "AXIS-Y",
    "category": ["Treatment", "Serums"]
  }
}
```

### Content fields (stored with the product, shown on storefront)
| Field | Required | Type | Where it shows |
|---|---|---|---|
| `lead` | **yes** | string | SHORT area: paragraph under the price |
| `benefits` | no | string[] (first 4) | SHORT area: tick list under lead |
| `description` | **yes** | string[] | LONG area: accordion **Description** (open by default) |
| `key_ingredients` | no | {name,text}[] | accordion **Key ingredients** |
| `how_to_use` | no | string[] | accordion **How to use** (`<ol class="howto">`) |
| `who_is_it_for` | no | {lead?,points[]} | **extra tab** Who is it for |
| `product_details` | no | {label,value}[] | **extra tab** Product details (`<table class="spec">`) |
| `storage` | no | string | line below Product details table |
| `full_ingredients` | no | string | **extra tab** Full ingredients |
| `delivery_returns` | no | string | Delivery & returns (else global text) |

Fixed accordion order: Description, Key ingredients, How to use, Who is it for, Product details, Full ingredients, Delivery & returns.
A section with no data is not rendered (except Delivery & returns, always rendered).

### `product` block (NOT stored as content; import helper only)
- Used **only to prefill the "Add New Product" form** (and optionally the Product tab of an existing product's modal, as a suggestion the owner can accept).
- **Never applied automatically** to an existing product. It is never read by the storefront.
- After parsing on an existing product, show an "Apply to product fields" button; it just fills the Product tab inputs. Nothing is saved until the owner presses Save there.

### Text safety / limits
Plain text only; React escapes output; no `dangerouslySetInnerHTML` for template data. Strip control chars, trim, collapse 3+ newlines.
Max lengths: lead 400, benefit 160, paragraph 2000, ingredient name 120 / text 400, step 400, full_ingredients 6000, total JSON 100 KB.

---

## 4. Upload formats (plugin must accept all three)

Extensions: `.json`, `.txt`, `.html`/`.htm`. Max 200 KB. Parse client-side (FileReader) for instant preview, **re-validate server-side in PHP before saving**.

The detailed format of JSON, TXT and HTML is defined in `SINGLE_PRODUCT_TEMPLATE_GUIDE.txt`. **That guide is the format contract. Implement the parsers exactly to it** (TXT `@section` markers with `Label :: Value` pairs; HTML `<section data-section="...">` plus fallback to demo `<details class="acc"><summary>` markup matched by summary text). Use `DOMParser` (JS) / `DOMDocument` (PHP); strip all tags/attributes, keep text only.

Parser returns `{ ok, template, errors[], warnings[] }`.
- `errors` (block save): invalid JSON, missing `lead`/`description`, file too large, version != 1.
- `warnings` (allow save): unknown sections, empty lists, benefits > 4 (extras ignored), `product` block present on an existing product (not auto-applied).
Unknown input is never silently dropped or guessed.

---

## 5. Storage and API

- Template JSON -> post meta `nje_product_template` (**no leading underscore**, so WooCommerce REST exposes it in `meta_data`). Also `nje_product_template_updated` (timestamp).
- Next.js already reads `/wp-json/wc/v3/products`. In the mapping, find `meta_data` key `nje_product_template`, `JSON.parse` in try/catch, re-validate shape, attach as `product.template`. Any failure -> `template: undefined` (default rendering). Never throw.
- Name/price/category/brand/size continue to flow through the existing mapping from WooCommerce. No special-casing.
- After any save/remove/create in plugin, trigger Next.js revalidation via the existing `/api/revalidate` route (reuse existing mechanism). Product page `revalidate = 60` is the safety net.
- Check every product fetch path in `wpgraphql.ts` (REST, GraphQL, fallback) for template mapping.

---

## 6. Product data written to WooCommerce (Edit and Add New)

All writes go through WooCommerce APIs so caches/lookups stay correct: `wc_get_product()` / `new WC_Product_Simple()` setters + `->save()`. Never write raw SQL.

| Field | Written to |
|---|---|
| Name | product title |
| Regular price | `set_regular_price()` |
| Sale price | `set_sale_price()` (empty = remove sale). Also keep `_price` consistent via `save()` |
| Category | taxonomy `product_cat` (multi-select; existing terms only unless owner confirms "create") |
| Brand | `product_brand` taxonomy if `taxonomy_exists('product_brand')`; otherwise product attribute named `Brand` (matches `extractBrand()` fallbacks). Select from existing terms; allow "create new" with confirm |
| Size | write wherever the current mapping reads `size` from (check `wpgraphql.ts`; likely attribute/meta). Document what you found in your report |
| Image (Add New, optional) | WP media picker -> featured image |

Validation: name required; prices numeric `> 0`; `sale_price <= regular_price`; category at least one for Add New.
Concurrency: show a toast on success/failure; on failure nothing is partially saved (wrap in try/catch, report which field failed).

---

## 7. Plugin admin UI

New top-level tab **"Products Content"** (follow how existing tabs register/render).

### 7.1 List
Top bar: search (title/SKU), filter chips `All / Template / Default`, primary button **+ Add New Product**.

Desktop table columns: **SL | Icon (thumbnail 40px, rounded) | Title | Status | Edit**
- SL = running number across pages. Status pill: `Template` (accent) / `Default` (muted).
- Row click or Edit icon opens the modal.
- **Pagination: 20 per page.** Prev/Next with inline **SVG chevron icons** (`<` `>`, optionally `<<` `>>` first/last) + "page X of Y". Disabled at ends.
- Server-side query (`wc_get_products` / `WP_Query` with `paged`, `s`, meta filter) over AJAX (`admin-ajax.php`, nonce + `manage_options`).
- **Mobile (<= 782px):** stacked cards (thumbnail, title, status, edit). No horizontal scroll.
- Empty state = icon + one short line.

### 7.2 Edit modal (from Edit)
Header: thumbnail + title + close (X). Tabs:
1. **Product** — Name, Regular price, Sale price, Category (multi-select), Brand (select), Size. **Save writes to WooCommerce (§6).** Shows current WooCommerce values on open (these are the source of truth).
2. **Template** — drag & drop zone + "Select file" (`.json,.txt,.html,.htm`). Compact error (red) / warning (amber) list. If file has a `product` block, show "Apply to product fields" (fills Product tab inputs only).
3. **Content** — forms for the sections of §3 (textareas; repeatable rows for ingredients/details with add/remove; ordered rows for steps). `lead` and `description` required.
4. **Preview** — renders the storefront accordion markup/CSS for the current template.

Footer: **Save**, **Remove template** (confirm; product returns to default rendering), **Download JSON**. Mobile: full-screen sheet, sticky footer.

### 7.3 Add New Product (modal, same style)
Fields (only these): **Name\*, Regular price\*, Sale price, Category\*, Brand, Size, Image (optional), Template import (drag/drop or select).**
- Importing a file parses it; the `product` block prefills Name/Prices/Size/Brand/Category (owner can edit); content sections attach as the template.
- Brand/Category from file that do not exist yet: shown as "new" chips; created on save only after owner confirms.
- Buttons: **Save as draft**, **Publish**. Created via `WC_Product_Simple`, status `draft`/`publish`, stock status `instock`.
- After creation the list refreshes and the product appears with correct status.

Security everywhere: nonce on every AJAX call, `current_user_can('edit_products')` (or `manage_options`), sanitize all inputs server-side, specific `wp_send_json_error` messages.

---

## 8. Storefront rendering (`ProductDetailClient.tsx`)

```
const t = product.template;
if (!t) {
  // EXISTING CODE PATH, UNTOUCHED (short paragraph, Description & Highlights,
  // Key Ingredients, How to Use, Delivery & Exchange - exactly as today)
} else {
  // SHORT area (replaces the paragraph under the price):
  //   t.lead as paragraph + t.benefits (first 4) as tick list
  // LONG area (replaces the info accordions):
  //   Description (open), Key ingredients, How to use,
  //   Who is it for, Product details, Full ingredients  (only those with data)
  //   Delivery & returns: t.delivery_returns ?? productSettings.delivery_text ?? existing default
}
```
- Everything not listed (brand link, h1, size, price row with sale/save, buy buttons, trust badges, concerns box, sticky bar, related products) is rendered **identically in both branches**, from WooCommerce data.
- Markup must match demo `(9)`: classes `acc`, `acc-body`, `ing`, `howto`, `who`, `spec`, `full-ing`.
- Optionally use `template.lead` as meta description in `generateMetadata`. Product JSON-LD unchanged.

---

## 9. Deliverables

1. Plugin changes + version bump + rebuilt zips (`create-plugin-zip.ps1`).
2. Next.js changes + `npm run build` with no type/lint errors, then `create-zip.ps1`.
3. Ship `SINGLE_PRODUCT_TEMPLATE_GUIDE.txt` inside the plugin (`docs/`) and a small "Download guide" text link in the Products Content tab.
4. Downloadable `sample.json`, `sample.txt`, `sample.html` (real AXIS-Y example from the guide) from the same tab.
5. A short report: size-field location found, brand taxonomy vs attribute used, and the T-test table.

---

## 10. Acceptance tests (all must pass; report as pass/fail table)

| # | Test | Expected |
|---|---|---|
| T1 | Product without template | Page identical to before (compare DOM) |
| T2 | Upload valid JSON | Short area = lead+benefits; long area shows all tabs in fixed order, Description open |
| T3 | Same content as TXT | Identical to T2 |
| T4 | Same content as HTML | Identical to T2 |
| T5 | Template without `delivery_returns` | Global plugin delivery text |
| T6 | Template with `delivery_returns` | Template text shown |
| T7 | Template without `who_is_it_for` | No "Who is it for" tab |
| T8 | Missing `lead` or `description` | Save blocked with clear error |
| T9 | Broken JSON | Error shown, nothing saved, page unaffected |
| T10 | Edit price/name in Product tab, Save | WooCommerce updated; storefront, cart, checkout all show new price |
| T11 | Template file contains `product` block, uploaded to existing product | Nothing changes in WooCommerce unless owner applies and saves in Product tab |
| T12 | Storefront price/name/brand/category with and without template | Always the WooCommerce value |
| T13 | Remove template | Default rendering restored; WooCommerce data untouched |
| T14 | Corrupt `nje_product_template` meta | Default rendering, no crash |
| T15 | Add New Product (manual fields) | WC product created with name, prices, category, brand; appears in list and on site (if published) |
| T16 | Add New Product via file import | Fields prefilled from `product` block, template attached, creates correctly |
| T17 | Unknown brand/category in import | Shown as "new", created only after confirm |
| T18 | List with 45 products | Pages 20/20/5, chevrons, search, filters work |
| T19 | Mobile 375px | List = cards, modals = full-screen, accordions same order as desktop |
| T20 | Non-privileged user calls AJAX | Rejected |
| T21 | `<script>alert(1)</script>` in any field | Shown as plain text, never executed |

---

## 11. Do NOT

- Do not remove or alter "Single Product Page Content & Policies".
- Do not read name/price/category/brand from the template for the storefront.
- Do not auto-apply the template's `product` block to an existing product.
- Do not add hint/description paragraphs in the plugin UI.
- Do not store the template globally; it is per-product meta.
- Do not use `dangerouslySetInnerHTML` for template data.
- Do not silently drop or auto-correct unknown input.
- Do not rebuild/zip until all tests pass.
