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.
Installation
Section titled “Installation”npm install @glyph-sdk/webyarn add @glyph-sdk/webpnpm add @glyph-sdk/webEnvironment Variables
Section titled “Environment Variables”Create a .env.local file in your project root:
NEXT_PUBLIC_GLYPH_API_KEY=gk_your_public_keyGLYPH_API_KEY=gk_your_secret_keyTypeScript Setup
Section titled “TypeScript Setup”Add type declarations for the web component:
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"]}App Router (Next.js 13+)
Section titled “App Router (Next.js 13+)”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.
1. Create a Client Component Wrapper
Section titled “1. Create a Client Component Wrapper”'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';2. Use in a Page
Section titled “2. Use in a Page”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> );}'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> );}3. Server-Side PDF Generation (API Route)
Section titled “3. Server-Side PDF Generation (API Route)”For secure PDF generation, create an API route that keeps your secret key on the server:
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);};Pages Router (Next.js 12 and earlier)
Section titled “Pages Router (Next.js 12 and earlier)”If you’re using the Pages Router, the setup is similar but uses _app.tsx for SDK registration.
1. Register the SDK
Section titled “1. Register the SDK”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} />;}2. Create a Quote Page
Section titled “2. Create a Quote Page”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> );}3. API Route for PDF Generation
Section titled “3. API Route for PDF Generation”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' }); }}Dynamic Imports (Avoiding SSR Issues)
Section titled “Dynamic Imports (Avoiding SSR Issues)”If you encounter hydration errors, use Next.js dynamic imports with SSR disabled:
'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} />;}With Server Actions (Next.js 14+)
Section titled “With Server Actions (Next.js 14+)”Use Server Actions for a cleaner PDF generation flow:
'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 componentimport { 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);};Styling with Tailwind CSS
Section titled “Styling with Tailwind CSS”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:
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;}Error Handling
Section titled “Error Handling”Wrap your editor with an error boundary for production resilience:
'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>Complete Example Repository
Section titled “Complete Example Repository”For a full working example with both App Router and Pages Router implementations, see our Next.js starter template.
Next Steps
Section titled “Next Steps”- React Integration - Detailed React patterns with hooks
- Custom Templates - Build your own document templates
- API Reference - Complete API documentation
- Theming - Customize the editor appearance