Appearance
Install
bash
npm install @maillune/sdkHow authentication works
The api-key attribute must be a session token — a short-lived JWT issued by your own backend. Never use your secret API key in client-side code.
Browser → GET /api/editor-session → Your server → POST /v1/sessions → Maillune API
↓
Browser ←──────────── sessionToken (2-hour JWT) ────────────────────See Authentication for the full flow.
Server-side token endpoint
Add one route to your backend (runs server-side only):
ts
// app/api/editor-session/route.ts (Next.js App Router)
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; // or your auth lib
export async function GET() {
const session = await getServerSession();
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { sessionToken } = await fetch('https://api.maillune.com/v1/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MAILLUNE_SECRET_KEY}`, // server-only
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId: session.user.id }),
}).then(r => r.json());
return NextResponse.json({ sessionToken });
}Never use NEXT_PUBLIC_ prefix for your Maillune key
NEXT_PUBLIC_* variables are embedded into the browser bundle. Always use a plain MAILLUNE_SECRET_KEY server-only variable and route through a backend endpoint.
EmailEditor component
tsx
// components/EmailEditor.tsx
'use client'; // Next.js App Router — required for browser APIs
import { useEffect, useRef, useState } from 'react';
import '@maillune/sdk';
import type { EmailBuilderElement, BlockTree } from '@maillune/sdk';
interface Props {
userId: string;
initialTemplate?: BlockTree;
onSave: (json: BlockTree) => Promise<void>;
}
export function EmailEditor({ userId, initialTemplate, onSave }: Props) {
const ref = useRef<EmailBuilderElement>(null);
const [sessionToken, setSessionToken] = useState('');
// Fetch session token from your backend on mount
useEffect(() => {
fetch('/api/editor-session')
.then(r => r.json())
.then(({ sessionToken }) => setSessionToken(sessionToken));
}, []);
// Load initial template once the element is ready
useEffect(() => {
if (initialTemplate && ref.current) {
ref.current.load(initialTemplate);
}
}, [initialTemplate, sessionToken]);
// Wire up save event
useEffect(() => {
const el = ref.current;
if (!el || !sessionToken) return;
const handler = (e: Event) => {
const { json } = (e as CustomEvent<{ json: BlockTree }>).detail;
onSave(json);
};
el.addEventListener('eb:save', handler);
return () => el.removeEventListener('eb:save', handler);
}, [onSave, sessionToken]);
if (!sessionToken) return <div>Loading editor…</div>;
return (
<div style={{ height: '100vh' }}>
<email-builder
ref={ref}
api-key={sessionToken}
user-id={userId}
theme="auto"
/>
</div>
);
}JSX types
Add to globals.d.ts (or any .d.ts file in your project):
ts
import type { EmailBuilderElement } from '@maillune/sdk';
declare global {
namespace JSX {
interface IntrinsicElements {
'email-builder': {
'api-key': string;
'user-id': string;
'template-id'?: string;
theme?: 'light' | 'dark' | 'auto';
ref?: React.Ref<EmailBuilderElement>;
style?: React.CSSProperties;
className?: string;
};
}
}
}Next.js (App Router) page
tsx
// app/editor/page.tsx
import { EmailEditor } from '@/components/EmailEditor';
import { getServerSession } from 'next-auth';
export default async function EditorPage() {
const session = await getServerSession();
return (
<EmailEditor
userId={session!.user.id}
onSave={saveTemplate}
/>
);
}With template persistence
tsx
'use client';
import { useState, useEffect } from 'react';
import type { BlockTree } from '@maillune/sdk';
import { EmailEditor } from '@/components/EmailEditor';
export function EditorPage({ templateId, userId }: { templateId: string; userId: string }) {
const [template, setTemplate] = useState<BlockTree | null>(null);
useEffect(() => {
fetch(`/api/templates/${templateId}`)
.then(r => r.json())
.then(data => setTemplate(data.blockTree));
}, [templateId]);
const handleSave = async (json: BlockTree) => {
await fetch(`/api/templates/${templateId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ blockTree: json }),
});
};
if (!template) return <div>Loading…</div>;
return (
<EmailEditor
userId={userId}
initialTemplate={template}
onSave={handleSave}
/>
);
}