Skip to content

Next.js Integration

This guide shows how to integrate Glyph into a Next.js application. We cover both the App Router (recommended for Next.js 13+) and Pages Router patterns, with full TypeScript support.

Terminal window
npm install @glyph-sdk/web

Create a .env.local file in your project root:

.env.local
NEXT_PUBLIC_GLYPH_API_KEY=gk_your_public_key
GLYPH_API_KEY=gk_your_secret_key

Add type declarations for the web component:

types/glyph.d.ts
declare namespace JSX {
interface IntrinsicElements {
'glyph-editor': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'api-key'?: string;
template?: string;
data?: string;
'base-url'?: string;
theme?: 'light' | 'dark' | 'system';
},
HTMLElement
>;
}
}
interface GlyphEditorElement extends HTMLElement {
setData(data: object): void;
setTemplate(templateId: string): void;
getSessionId(): string | null;
getHtml(): string;
modify(prompt: string, options?: { region?: string }): Promise<void>;
generatePdf(options?: object): Promise<Blob>;
generatePng(options?: object): Promise<Blob>;
undo(): void;
redo(): void;
}

Add the types to your tsconfig.json:

{
"compilerOptions": {
// ...existing options
},
"include": ["types/**/*.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

The App Router uses React Server Components by default. Since Glyph’s editor is a client-side web component, you’ll need to use the 'use client' directive.

app/components/GlyphEditor.tsx
'use client';
import { useRef, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
// Import the SDK (client-side only)
if (typeof window !== 'undefined') {
import('@glyph-sdk/web');
}
export interface QuoteData {
client: {
name: string;
company?: string;
email?: string;
};
lineItems: Array<{
description: string;
quantity: number;
unitPrice: number;
total: number;
}>;
totals: {
subtotal: number;
tax?: number;
total: number;
};
meta?: {
quoteNumber?: string;
date?: string;
};
}
export interface GlyphEditorProps {
apiKey: string;
template?: string;
data: QuoteData;
baseUrl?: string;
theme?: 'light' | 'dark' | 'system';
className?: string;
onReady?: () => void;
onModified?: (detail: { html: string; prompt: string }) => void;
onError?: (detail: { code: string; message: string }) => void;
}
export interface GlyphEditorRef {
modify: (prompt: string, options?: { region?: string }) => Promise<void>;
generatePdf: (options?: object) => Promise<Blob>;
getSessionId: () => string | null;
getHtml: () => string;
undo: () => void;
redo: () => void;
}
export const GlyphEditor = forwardRef<GlyphEditorRef, GlyphEditorProps>(
({ apiKey, template = 'quote-modern', data, baseUrl, theme = 'system', className, onReady, onModified, onError }, ref) => {
const editorRef = useRef<GlyphEditorElement>(null);
useImperativeHandle(ref, () => ({
modify: (prompt, options) => editorRef.current!.modify(prompt, options),
generatePdf: (options) => editorRef.current!.generatePdf(options),
getSessionId: () => editorRef.current!.getSessionId(),
getHtml: () => editorRef.current!.getHtml(),
undo: () => editorRef.current!.undo(),
redo: () => editorRef.current!.redo(),
}));
const handleReady = useCallback(() => onReady?.(), [onReady]);
const handleModified = useCallback((e: CustomEvent) => onModified?.(e.detail), [onModified]);
const handleError = useCallback((e: CustomEvent) => onError?.(e.detail), [onError]);
useEffect(() => {
const editor = editorRef.current;
if (!editor) return;
editor.addEventListener('glyph:ready', handleReady);
editor.addEventListener('glyph:modified', handleModified as EventListener);
editor.addEventListener('glyph:error', handleError as EventListener);
return () => {
editor.removeEventListener('glyph:ready', handleReady);
editor.removeEventListener('glyph:modified', handleModified as EventListener);
editor.removeEventListener('glyph:error', handleError as EventListener);
};
}, [handleReady, handleModified, handleError]);
return (
<glyph-editor
ref={editorRef}
api-key={apiKey}
template={template}
data={JSON.stringify(data)}
base-url={baseUrl}
theme={theme}
className={className}
/>
);
}
);
GlyphEditor.displayName = 'GlyphEditor';
app/quotes/page.tsx
import { QuoteEditor } from './QuoteEditor';
export const metadata = {
title: 'Create Quote | My App',
};
export default function QuotesPage() {
return (
<main className="container mx-auto py-8">
<h1 className="text-2xl font-bold mb-6">Create a Quote</h1>
<QuoteEditor />
</main>
);
}
app/quotes/QuoteEditor.tsx
'use client';
import { useRef, useState } from 'react';
import { GlyphEditor, GlyphEditorRef, QuoteData } from '../components/GlyphEditor';
const sampleData: QuoteData = {
client: {
name: 'Acme Corporation',
company: 'Acme Corp',
email: 'billing@acme.com',
},
lineItems: [
{ description: 'Website Redesign', quantity: 1, unitPrice: 8000, total: 8000 },
{ description: 'SEO Optimization', quantity: 1, unitPrice: 2000, total: 2000 },
],
totals: {
subtotal: 10000,
tax: 800,
total: 10800,
},
meta: {
quoteNumber: 'Q-2025-001',
date: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
},
};
export function QuoteEditor() {
const editorRef = useRef<GlyphEditorRef>(null);
const [prompt, setPrompt] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleModify = async () => {
if (!prompt.trim() || !editorRef.current) return;
setIsLoading(true);
try {
await editorRef.current.modify(prompt);
setPrompt('');
} finally {
setIsLoading(false);
}
};
const handleDownload = async () => {
if (!editorRef.current) return;
const blob = await editorRef.current.generatePdf();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'quote.pdf';
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="space-y-4">
<div className="flex gap-2">
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleModify()}
placeholder="Describe changes (e.g., 'Make the header navy blue')"
className="flex-1 px-4 py-2 border rounded-lg"
disabled={isLoading}
/>
<button
onClick={handleModify}
disabled={isLoading || !prompt.trim()}
className="px-4 py-2 bg-purple-600 text-white rounded-lg disabled:opacity-50"
>
{isLoading ? 'Applying...' : 'Apply'}
</button>
<button
onClick={handleDownload}
className="px-4 py-2 bg-gray-800 text-white rounded-lg"
>
Download PDF
</button>
</div>
<GlyphEditor
ref={editorRef}
apiKey={process.env.NEXT_PUBLIC_GLYPH_API_KEY!}
template="quote-modern"
data={sampleData}
className="h-[700px] rounded-lg shadow-lg"
onReady={() => console.log('Editor ready')}
onError={({ message }) => console.error('Glyph error:', message)}
/>
</div>
);
}

For secure PDF generation, create an API route that keeps your secret key on the server:

app/api/generate-pdf/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { html, sessionId } = await request.json();
if (!html && !sessionId) {
return NextResponse.json(
{ error: 'Either html or sessionId is required' },
{ status: 400 }
);
}
const response = await fetch('https://api.glyph.you/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GLYPH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
sessionId,
format: 'pdf',
}),
});
if (!response.ok) {
const error = await response.json();
return NextResponse.json(error, { status: response.status });
}
const pdfBuffer = await response.arrayBuffer();
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="document.pdf"',
},
});
} catch (error) {
console.error('PDF generation error:', error);
return NextResponse.json(
{ error: 'Failed to generate PDF' },
{ status: 500 }
);
}
}

Use the API route from your client component:

const handleServerDownload = async () => {
const html = editorRef.current?.getHtml();
if (!html) return;
const response = await fetch('/api/generate-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html }),
});
if (!response.ok) {
throw new Error('Failed to generate PDF');
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'quote.pdf';
a.click();
URL.revokeObjectURL(url);
};

If you’re using the Pages Router, the setup is similar but uses _app.tsx for SDK registration.

pages/_app.tsx
import type { AppProps } from 'next/app';
import { useEffect } from 'react';
import '../styles/globals.css';
export default function App({ Component, pageProps }: AppProps) {
useEffect(() => {
// Import SDK on client-side only
import('@glyph-sdk/web');
}, []);
return <Component {...pageProps} />;
}
pages/quotes/index.tsx
import { useRef, useState } from 'react';
import type { QuoteData, GlyphEditorRef } from '../../types/glyph';
const sampleData: QuoteData = {
client: {
name: 'Tech Startup Inc.',
email: 'hello@techstartup.io',
},
lineItems: [
{ description: 'MVP Development', quantity: 1, unitPrice: 25000, total: 25000 },
{ description: 'Cloud Infrastructure', quantity: 3, unitPrice: 500, total: 1500 },
],
totals: {
subtotal: 26500,
total: 26500,
},
meta: {
quoteNumber: 'Q-2025-042',
date: 'January 20, 2025',
},
};
export default function QuotesPage() {
const editorRef = useRef<GlyphEditorElement>(null);
const [isReady, setIsReady] = useState(false);
const [prompt, setPrompt] = useState('');
const handleModify = async () => {
if (!prompt.trim() || !editorRef.current) return;
await editorRef.current.modify(prompt);
setPrompt('');
};
const handleDownload = async () => {
if (!editorRef.current) return;
const blob = await editorRef.current.generatePdf();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'quote.pdf';
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="container">
<h1>Create Quote</h1>
<div className="toolbar">
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleModify()}
placeholder="Describe changes..."
/>
<button onClick={handleModify}>Apply</button>
<button onClick={handleDownload} disabled={!isReady}>
Download PDF
</button>
</div>
<glyph-editor
ref={editorRef}
api-key={process.env.NEXT_PUBLIC_GLYPH_API_KEY}
base-url="https://api.glyph.you"
template="quote-modern"
data={JSON.stringify(sampleData)}
onGlyphReady={() => setIsReady(true)}
/>
<style jsx>{`
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.toolbar {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.toolbar input {
flex: 1;
padding: 0.5rem 1rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
}
.toolbar button {
padding: 0.5rem 1rem;
background: #7c3aed;
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
}
.toolbar button:disabled {
opacity: 0.5;
}
glyph-editor {
height: 700px;
display: block;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
`}</style>
</div>
);
}
pages/api/generate-pdf.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { html, sessionId } = req.body;
if (!html && !sessionId) {
return res.status(400).json({ error: 'Either html or sessionId is required' });
}
try {
const response = await fetch('https://api.glyph.you/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GLYPH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, sessionId, format: 'pdf' }),
});
if (!response.ok) {
const error = await response.json();
return res.status(response.status).json(error);
}
const buffer = Buffer.from(await response.arrayBuffer());
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="document.pdf"');
res.send(buffer);
} catch (error) {
console.error('PDF generation error:', error);
res.status(500).json({ error: 'Failed to generate PDF' });
}
}

If you encounter hydration errors, use Next.js dynamic imports with SSR disabled:

app/components/DynamicGlyphEditor.tsx
'use client';
import dynamic from 'next/dynamic';
import type { GlyphEditorProps } from './GlyphEditor';
const GlyphEditor = dynamic(
() => import('./GlyphEditor').then((mod) => mod.GlyphEditor),
{
ssr: false,
loading: () => (
<div className="h-[700px] bg-gray-100 animate-pulse rounded-lg flex items-center justify-center">
Loading editor...
</div>
),
}
);
export function DynamicGlyphEditor(props: GlyphEditorProps) {
return <GlyphEditor {...props} />;
}

Use Server Actions for a cleaner PDF generation flow:

app/actions/pdf.ts
'use server';
export async function generatePdf(html: string): Promise<{ url: string } | { error: string }> {
try {
const response = await fetch('https://api.glyph.you/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GLYPH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, format: 'pdf' }),
});
if (!response.ok) {
const error = await response.json();
return { error: error.message || 'Generation failed' };
}
const buffer = await response.arrayBuffer();
const base64 = Buffer.from(buffer).toString('base64');
return { url: `data:application/pdf;base64,${base64}` };
} catch (error) {
return { error: 'Failed to generate PDF' };
}
}
// In your client component
import { generatePdf } from '../actions/pdf';
const handleServerAction = async () => {
const html = editorRef.current?.getHtml();
if (!html) return;
const result = await generatePdf(html);
if ('error' in result) {
console.error(result.error);
return;
}
// Open the PDF
window.open(result.url);
};

The Glyph editor accepts CSS variables for theming. With Tailwind:

<GlyphEditor
className="h-[700px] rounded-lg shadow-lg overflow-hidden
[--glyph-accent:theme(colors.purple.600)]
[--glyph-radius:theme(borderRadius.lg)]"
{...props}
/>

Or in your global CSS:

app/globals.css
glyph-editor {
--glyph-accent: #7c3aed;
--glyph-accent-hover: #6d28d9;
--glyph-radius: 0.5rem;
--glyph-font: var(--font-sans);
}
/* Dark mode support */
.dark glyph-editor,
[data-theme="dark"] glyph-editor {
--glyph-bg: #1e293b;
--glyph-text: #f1f5f9;
--glyph-border: #334155;
}

Wrap your editor with an error boundary for production resilience:

app/components/GlyphErrorBoundary.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
}
export class GlyphErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-8 bg-red-50 text-red-800 rounded-lg">
<h2 className="font-semibold">Document editor unavailable</h2>
<p>Please refresh the page to try again.</p>
</div>
);
}
return this.props.children;
}
}
// Usage
<GlyphErrorBoundary>
<GlyphEditor {...props} />
</GlyphErrorBoundary>

For a full working example with both App Router and Pages Router implementations, see our Next.js starter template.