@markopolo_ai_inc/markopolo-email-editor
v1.0.68
Published
Drag-and-drop React email template editor: blocks, live preview, export as JSON or HTML. Built with Create React App for the demo app; the **published package** is produced with Rollup (`npm run build:lib` → `lib/`).
Downloads
3,988
Readme
@markopolo_ai_inc/markopolo-email-editor
Drag-and-drop React email template editor: blocks, live preview, export as JSON or HTML. Built with Create React App for the demo app; the published package is produced with Rollup (npm run build:lib → lib/).
Install
Your app must provide peer dependencies (single React tree, one copy of Ant Design / Font Awesome, etc.):
npm install @markopolo_ai_inc/markopolo-email-editor react react-dom antd @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome framer-motion react-color clsx axiosAlways import the editor and its CSS:
import EmailEditor, {
exportTemplateJson,
validateExportPayload,
exportNormalizerToHtml,
TEMPLATE_DATA_SCHEMA,
createDefaultTemplateData,
validateTemplateDataShape,
} from "@markopolo_ai_inc/markopolo-email-editor";
import "@markopolo_ai_inc/markopolo-email-editor/lib/index.css";Quick start
import { useRef } from "react";
import EmailEditor, {
exportTemplateJson,
} from "@markopolo_ai_inc/markopolo-email-editor";
import "@markopolo_ai_inc/markopolo-email-editor/lib/index.css";
export function TemplateEditor({ apiConfig = {} }) {
const editorRef = useRef(null);
const handleSave = () => {
const { payload, issues } = exportTemplateJson(editorRef, {
validate: true,
});
const errors = issues.filter((i) => i.severity === "error");
if (errors.length) return;
// payload: { blockList, bodySettings, aiContentRefs }
};
return (
<>
<button type="button" onClick={handleSave}>
Save
</button>
<EmailEditor
ref={editorRef}
defaultBlockList={initialBlockList}
defaultBodySettings={initialBodySettings}
templateData={templateData}
language="en"
apiConfig={apiConfig}
/>
</>
);
}Main props
| Prop | Type | Default | Description |
| ------------------------- | ------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| defaultBlockList | array or { blockList, bodySettings, aiContentRefs } | — | Initial template (see Template shape below). |
| defaultBodySettings | object | from template / built-in | Global email body (width, colors, font, preheader). |
| fillProductSlots | boolean | true | Template-level product slot policy (also read from defaultBlockList.fillProductSlots when loading saved JSON). Exported at the top level of exportJson() / save payloads alongside blockList. When true, product blocks fill slots using cart → view → cross_sell → upsell → discount → popular; when false, only cart items fill slots (otherwise omitted at send). |
| aiContentRefs | object | {} | Optional AI metadata map when loading saved templates. |
| templateData | object | — | Live data for products, company, brand, placeholders (see templateData). |
| language | string | "en" | UI language key. |
| customLanguageLibraries | object | — | Extra i18n strings. |
| variableDefinitions | object | built-in | Override placeholder metadata. |
| apiConfig | object | {} | Usually companyId + headers only; upload / products / ML URLs default inside the package. See apiConfig. |
| aiEnabled | boolean | true | If false, the sidebar hides the “AI powered” text entry and the Write using AI switch and prompts in text settings. Existing dynamic blocks still export as before; users simply cannot turn AI mode on from the UI. |
Ref API: ref.current.exportJson(), ref.current.exportHtml(), plus blockList / bodySettings / aiContentRefs mirrors for reading state.
templateData
Pass backend data as templateData so blocks and HTML export resolve placeholders consistently.
| Area | Path | Role |
| ------------ | -------------------------------------------------------- | ------------------ |
| Products | content.products, content.primaryProductId | Product containers |
| Menu | companyContext.navigation_links | Menu blocks |
| Social | companyContext.social_links | Social blocks |
| Branding | brandContext.brand_color | Default CTA / button background color |
| Placeholders | brandContext, companyContext, content, companyId | Export / preview |
Helpers (optional): TEMPLATE_DATA_SCHEMA, createDefaultTemplateData(overrides), validateTemplateDataShape(data) → { ok, warnings }.
Keep templateData.companyId in sync with apiConfig.companyId when you scope by company.
If a button/CTA block has no explicit background color set, the editor preview and HTML export fall back to templateData.brandContext.brand_color.
apiConfig
The editor merges your apiConfig with built-in Markopolo-style endpoints (default host https://api-alpha.markopolo.ai/v1):
…/upload-file(queryshow=false, company id as form fieldcomment)…/knowledge-base/products/{companyId}(page,limit,search— used by the column product picker + Dashboard seed)- AI text (dynamic blocks) uses a separate ML host by default:
…/ml-service/content/generate-email-nodesonhttps://nbq-ml-api-stg.markopolo.ai/v1(override withmlGenerationApiBaseUrlV1orREACT_APP_ML_API_BASE_V1). Legacy…/ml/generate-contenton the alpha API is not defaulted; setmlGenerationEndpointonly if you still use that route. emailTemplateIdis set by default to450e1238-e962-4ef0-a854-817583f6e75d(DEFAULT_EMAIL_TEMPLATE_ID); it is sent in the ML request body asemailTemplateId. Override withapiConfig.emailTemplateIdorREACT_APP_EMAIL_TEMPLATE_ID.
You normally only pass:
| Field | Role |
| --------------- | ----------------------------------------------- |
| companyId | Company / resource id (catalog + upload scope). |
| headers | e.g. { Authorization: 'Bearer …' }. |
Optional:
| Field | Role |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| apiBaseUrlV1 (alias apiBaseV1) | Non-default alpha API root (upload + products). ML text uses mlGenerationApiBaseUrlV1 unless you override mlEmailNodesEndpoint. |
| emailTemplateId | custom_email_templates id in the generate-email-nodes JSON body; defaults to 450e1238-e962-4ef0-a854-817583f6e75d unless you override (or set REACT_APP_EMAIL_TEMPLATE_ID). |
| mlGenerationApiBaseUrlV1 (alias mlApiBaseUrlV1) | ML service root for generate-email-nodes (default staging nbq-ml host). |
| imageUploadEndpoint, productsEndpoint, mlEmailNodesEndpoint, mlGenerationEndpoint (legacy only), imageUploadQueryParams, imageUploadCompanyField | Override defaults when needed. |
Typical integration:
<EmailEditor
ref={editorRef}
defaultBlockList={blocks}
templateData={{ ...templateData, companyId: resourceId }}
apiConfig={{
companyId: resourceId,
headers: { Authorization: `Bearer ${token}` },
}}
/>If you load products outside EmailEditor, you can build the same URLs with mergeMarkopoloApiConfig (exported from the package). The merged object includes emailTemplateId for the ML API unless you pass a different value.
import { mergeMarkopoloApiConfig, DEFAULT_EMAIL_TEMPLATE_ID } from "@markopolo_ai_inc/markopolo-email-editor";
// mergeMarkopoloApiConfig({}).emailTemplateId === DEFAULT_EMAIL_TEMPLATE_IDThe repo’s demo Dashboard merges its companyId prop into apiConfig before EmailEditor.
Export JSON (no demo Header)
Use a ref and exportTemplateJson (or ref.current.exportJson()):
import { useRef } from "react";
import EmailEditor, {
exportTemplateJson,
} from "@markopolo_ai_inc/markopolo-email-editor";
const { payload, issues } = exportTemplateJson(editorRef, { validate: true });
// validate: false → same payload shape, issues: []validateExportPayload
Same rules as the export modal; pure function:
import { validateExportPayload } from "@markopolo_ai_inc/markopolo-email-editor";
const issues = validateExportPayload({ blockList, bodySettings });Issue objects include severity, message, and a human-readable location (breadcrumb path with →, e.g. Text — Discount → 3rd of 5 stacked blocks).
JSON → HTML
import { exportNormalizerToHtml } from "@markopolo_ai_inc/markopolo-email-editor";
const html = exportNormalizerToHtml({ blockList, bodySettings, templateData });Template shape for defaultBlockList
- Legacy:
defaultBlockList={[ ...blocks ]} - Export object:
defaultBlockList={{ blockList, bodySettings, aiContentRefs }}(orblocksinstead ofblockList).
Custom variable definitions
Override defaults by passing variableDefinitions: { global: [...], product: [...], discount: [...] } with key, label, labelKey, source, type, token, etc. (see package examples).
Scripts (this repo)
| Script | Purpose |
| --------------------------- | -------------------------------------------------------------------- |
| npm run build:lib | Rollup → lib/index.js, lib/index.mjs, lib/index.css (publish). |
| npm start / npm run dev | CRA demo (local development). |
Install in this repo: npm install.
Note on the demo app
src/pages/Dashboard (including its Header and export modal) is for local development only and is not part of the published public API. Integrate export and validation in your app as shown above. The demo merges companyId into apiConfig and uses the same built-in API defaults as EmailEditor.
