AMP Block
AMP for Email blocks are interactive components that render only in AMP Email (e.g. Gmail). The editor can export either normal HTML (mjml) or AMP Email (amp-mjml). This page summarizes what developers need to know to use them.
Integration overview​
To integrate AMP Email in your app:
- Register AMP plugins with
PluginManager(Accordion, Carousel, Form, Product, Reviews, LuckyWheel fromeasy-email-pro-kit). - Export with
outputFormat: "amp-mjml"when callingEditorCore.toMJMLso AMP blocks are included in the output. - Convert to final HTML using the
mjml2amppackage. AMP requires explicit width/height for images, so you must provide animageDimensionsmap (see Server-side rendering and image dimensions below).
For a complete working example: the Full demo (demo/src/examples/Full/index.tsx) registers all AMP plugins; the export logic for "Export AMP MJML" and "Export AMP Email" is in demo/src/components/EditorHeader.tsx (onExportAmpMJML, onExportAmpEmail), including getImageUrlsForAmp, image dimension resolution, mjml2amp, and mergetags rendering.
Concepts​
- AMP for Email: Format that requires AMP runtime and allowed scripts; supported by a subset of clients (e.g. Gmail, Mail.ru).
- Output format: When exporting or previewing, you choose:
mjml— normal HTML emailamp-mjml— AMP Email HTML (needed for AMP blocks to work in client)
- Output target (per block): Each block can be "All", "HTML only", or "AMP only". AMP-only blocks are omitted when exporting as
mjmlunless they have fallback enabled (then a simple MJML/static version is shown for non-AMP clients).
Kit AMP blocks​
Provided by easy-email-pro-kit:
| Block | Type constant | Typical use |
|---|---|---|
| Accordion | AMP_ACCORDION | Expand/collapse sections |
| Carousel | AMP_CAROUSEL | Image slider (optional thumbs) |
| Form | AMP_FORM | Submit form (action-xhr) |
| Product | AMP_PRODUCT | Product carousel + color picker |
| Reviews | AMP_REVIEWS | Star rating + feedback form |
| Lucky Wheel | AMP_LUCKY_WHEEL | Spin-the-wheel interaction |
Form and Reviews need an action-xhr URL and return JSON for success/error (used with amp-mustache). Other blocks rely on AMP components such as amp-carousel, amp-selector, amp-accordion, amp-bind, etc.
How to use​
1. Register plugins​
Register the AMP plugins with PluginManager (same as other kit components):
import { PluginManager } from "easy-email-pro-core";
import {
AmpAccordionPlugin,
AmpCarouselPlugin,
AmpFormPlugin,
AmpProductPlugin,
AmpReviewsPlugin,
AmpLuckyWheelPlugin,
} from "easy-email-pro-kit";
PluginManager.registerPlugins([
AmpAccordionPlugin,
AmpCarouselPlugin,
AmpFormPlugin,
AmpProductPlugin,
AmpReviewsPlugin,
AmpLuckyWheelPlugin,
]);
2. Export as AMP​
When converting the template to MJML for sending, pass outputFormat: "amp-mjml" so AMP blocks are included. Use outputFormat: "mjml" for normal HTML; AMP-only blocks without fallback will then be skipped.
import { EditorCore } from "easy-email-pro-core";
const mjmlString = EditorCore.toMJML({
element: pageData,
mode: "production",
outputFormat: "amp-mjml", // or "mjml" for normal HTML
});
Server-side rendering and image dimensions​
Converting AMP MJML to final HTML is done with mjml2amp (e.g. the mjml2amp package). AMP requires explicit width/height for images, so mjml2amp needs an imageDimensions map (image URL → { width, height }). You must resolve image dimensions before calling mjml2amp.
- Server-side: There is no browser to load images. First collect all image URLs from the AMP MJML (e.g. using
getImageUrlsForAmp(ampMjmlString)frommjml2amp), then obtain dimensions for each URL (e.g. via HTTP range request, image-size library, or your CDN). After you haveimageDimensions, callmjml2amp(ampMjmlString, { imageDimensions, imageDimensionsStrict: false }). - Browser (e.g. editor preview): The theme uses
getImageUrlsForAmpto get URLs, then loads each image (e.g.new Image(),img.onload→naturalWidth/naturalHeight) and waits until all dimensions are ready before callingmjml2amp. SeeuseAmpImageDimensionsandPreviewEmailineasy-email-pro-themefor the in-editor flow.
Form / Reviews backend​
Form and Reviews blocks submit via action-xhr (POST). The action-xhr value can be a literal URL or a mergetag (variable); if not set, Form uses the variable AMP_FORM_ACTION_URL and Reviews uses AMP_REVIEWS_ACTION_URL, which are resolved at render time from mergetags data. Your endpoint must return JSON and set AMP for Email CORS headers. Success response keys (e.g. name, email, message) can be used in the block’s success template; error response must include a message key. See types AmpFormSubmitSuccessResponse, AmpFormSubmitErrorResponse, and AmpReviewsSubmitBody in easy-email-pro-kit.
Example API route (Next.js)​
The following example handles both Form and Reviews submissions in one endpoint (e.g. pages/api/form.ts). It uses isReviewsSubmitBody from the kit to distinguish Reviews (has rating) from a generic Form.
import type { NextApiRequest, NextApiResponse } from "next";
import {
isReviewsSubmitBody,
type AmpFormSubmitBody,
type AmpFormSubmitSuccessResponse,
type AmpFormSubmitErrorResponse,
type AmpReviewsSubmitSuccessResponse,
type AmpReviewsSubmitErrorResponse,
} from "easy-email-pro-kit";
/** Set AMP for Email CORS headers. Required for action-xhr. */
function setAmpCorsHeaders(req: NextApiRequest, res: NextApiResponse): boolean {
const ampEmailSender = req.headers["amp-email-sender"] as string | undefined;
const origin = req.headers["origin"] as string | undefined;
const ampSourceOrigin =
(req.query.__amp_source_origin as string) ??
(req.body && typeof req.body === "object" && "__amp_source_origin" in req.body
? String((req.body as Record<string, unknown>).__amp_source_origin)
: undefined);
if (ampEmailSender) {
res.setHeader("AMP-Email-Allow-Sender", ampEmailSender);
return true;
}
if (origin && ampSourceOrigin) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("AMP-Access-Control-Allow-Source-Origin", ampSourceOrigin);
res.setHeader(
"Access-Control-Expose-Headers",
"AMP-Access-Control-Allow-Source-Origin"
);
return true;
}
return false;
}
function toRecord(body: unknown): Record<string, string | string[] | undefined> {
if (body && typeof body === "object" && !Array.isArray(body)) {
const out: Record<string, string | string[] | undefined> = {};
for (const [k, v] of Object.entries(body)) {
if (typeof v === "string") out[k] = v;
else if (Array.isArray(v)) out[k] = v.map(String);
else if (v != null) out[k] = String(v);
}
return out;
}
return {};
}
function handleReviewsSubmit(
body: AmpFormSubmitBody,
res: NextApiResponse<AmpReviewsSubmitSuccessResponse | AmpReviewsSubmitErrorResponse>
) {
const ratingStr = Array.isArray(body.rating) ? body.rating[0] : body.rating;
const rating = ratingStr != null ? parseInt(String(ratingStr).trim(), 10) : NaN;
const feedback = (Array.isArray(body.feedback) ? body.feedback[0] : body.feedback) ?? "";
if (Number.isNaN(rating) || rating < 1 || rating > 5) {
res.status(400).json({
message: "Please select a star rating between 1 and 5.",
code: "INVALID_RATING",
});
return;
}
res.status(200).json({
message: "Thank you! Your feedback has been submitted successfully.",
rating: String(rating),
...(feedback ? { feedback: String(feedback).trim() } : {}),
});
}
function handleFormSubmit(
body: AmpFormSubmitBody,
res: NextApiResponse<AmpFormSubmitSuccessResponse | AmpFormSubmitErrorResponse>
) {
const name = Array.isArray(body.name) ? body.name[0] : body.name;
const email = Array.isArray(body.email) ? body.email[0] : body.email;
if (!name?.trim()) {
res.status(400).json({ message: "Name is required.", code: "MISSING_NAME" });
return;
}
if (!email?.trim()) {
res.status(400).json({ message: "Email is required.", code: "MISSING_EMAIL" });
return;
}
res.status(200).json({
name: String(name).trim(),
email: String(email).trim(),
message: "Thanks for subscribing!",
});
}
async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "POST") {
res.setHeader("Allow", "POST");
res.status(405).json({ message: "Method Not Allowed" });
return;
}
setAmpCorsHeaders(req, res);
const body = toRecord(req.body);
if (isReviewsSubmitBody(body)) {
handleReviewsSubmit(body, res);
return;
}
handleFormSubmit(body, res);
}
function allowCors(fn: (req: NextApiRequest, res: NextApiResponse) => Promise<void>) {
return async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS, POST");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, Accept, AMP-Email-Sender");
if (req.method === "OPTIONS") {
res.status(200).end();
return;
}
return fn(req, res);
};
}
export default allowCors(handler);