Skip to main content

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:

  1. Register AMP plugins with PluginManager (Accordion, Carousel, Form, Product, Reviews, LuckyWheel from easy-email-pro-kit).
  2. Export with outputFormat: "amp-mjml" when calling EditorCore.toMJML so AMP blocks are included in the output.
  3. Convert to final HTML using the mjml2amp package. AMP requires explicit width/height for images, so you must provide an imageDimensions map (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 email
    • amp-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 mjml unless 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:

BlockType constantTypical use
AccordionAMP_ACCORDIONExpand/collapse sections
CarouselAMP_CAROUSELImage slider (optional thumbs)
FormAMP_FORMSubmit form (action-xhr)
ProductAMP_PRODUCTProduct carousel + color picker
ReviewsAMP_REVIEWSStar rating + feedback form
Lucky WheelAMP_LUCKY_WHEELSpin-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) from mjml2amp), then obtain dimensions for each URL (e.g. via HTTP range request, image-size library, or your CDN). After you have imageDimensions, call mjml2amp(ampMjmlString, { imageDimensions, imageDimensionsStrict: false }).
  • Browser (e.g. editor preview): The theme uses getImageUrlsForAmp to get URLs, then loads each image (e.g. new Image(), img.onload → naturalWidth / naturalHeight) and waits until all dimensions are ready before calling mjml2amp. See useAmpImageDimensions and PreviewEmail in easy-email-pro-theme for 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);