▲
Next.js Integration
Full-stack React framework with SSR and API routes
React FrameworkSSR + SSGTypeScript
Quick Start
1. Project Setup
bash
Click to copy
# Create a new Next.js project
npx create-next-app@latest calligraphy-app --typescript --tailwind --eslint --app
# Navigate to project directory
cd calligraphy-app
# Install additional dependencies
npm install axios swr
npm install -D @types/node
# For image handling and downloads
npm install file-saver
npm install -D @types/file-saver2. Environment Configuration
bash
Click to copy
// .env.local
NEXT_PUBLIC_CALLIGRAPHY_API_BASE_URL=https://api.calligraphymaker.com/api/v2
CALLIGRAPHY_API_KEY=your_api_key_here
// For server-side API routes
CALLIGRAPHY_API_KEY_SERVER=your_server_api_key_here3. API Service Setup
typescript
Click to copy
// lib/calligraphy-api.ts
import axios, { AxiosResponse } from 'axios'
const API_BASE_URL = process.env.NEXT_PUBLIC_CALLIGRAPHY_API_BASE_URL
const API_KEY = process.env.NEXT_PUBLIC_CALLIGRAPHY_API_KEY || process.env.CALLIGRAPHY_API_KEY
// Types
export interface GenerateRequest {
text: string
language?: string
fontStyle?: string
count?: number
imageFormat?: string
}
export interface CalligraphyResult {
id: string
resultText: string
fontFamily: string
cdnUrl?: string
appliedVariants: AppliedVariant[]
}
export interface AppliedVariant {
type: string
position: number
character: string
}
export interface CalligraphyMetadata {
totalVariations: number
averageVariantsApplied: number
fontFamily: string
}
export interface CalligraphyUsage {
credits_used: number
remaining_credits: number
}
export interface CalligraphyResponse {
success: boolean
data: {
results: CalligraphyResult[]
metadata: CalligraphyMetadata
usage: CalligraphyUsage
}
}
export interface SvgDownloadRequest {
resultText: string
fontName: string
}
export interface SvgDownloadResponse {
success: boolean
data: {
resultText: string
fontFamily: string
svgBase64: string
svgString: string
dimensions: {
width: number
height: number
}
}
}
export interface ImageDownloadRequest {
resultText: string
fontName: string
imageWidth?: number
imageHeight?: number
imageQuality?: number
imageFormat?: string
backgroundColor?: string
textColor?: string
}
export interface ImageDownloadResponse {
success: boolean
data: {
resultText: string
fontName: string
format: string
imageBase64: string
dimensions: {
width: number
height: number
}
quality: number
backgroundColor: string
textColor: string
}
}
// API Client Class
export class CalligraphyAPI {
private static instance: CalligraphyAPI
private client = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
timeout: 30000
})
static getInstance(): CalligraphyAPI {
if (!CalligraphyAPI.instance) {
CalligraphyAPI.instance = new CalligraphyAPI()
}
return CalligraphyAPI.instance
}
async generateCalligraphy(request: GenerateRequest): Promise<CalligraphyResponse> {
try {
const response: AxiosResponse<CalligraphyResponse> = await this.client.post('/generate', {
text: request.text,
language: request.language || 'hindi',
fontStyle: request.fontStyle || 'calligraphy',
count: request.count || 3,
imageFormat: request.imageFormat || 'png'
})
return response.data
} catch (error) {
throw this.handleError(error)
}
}
async downloadSvg(request: SvgDownloadRequest): Promise<SvgDownloadResponse> {
try {
const response: AxiosResponse<SvgDownloadResponse> = await this.client.post('/download-svg', request)
return response.data
} catch (error) {
throw this.handleError(error)
}
}
async downloadImage(request: ImageDownloadRequest): Promise<ImageDownloadResponse> {
try {
const response: AxiosResponse<ImageDownloadResponse> = await this.client.post('/download-image', {
resultText: request.resultText,
fontName: request.fontName,
imageWidth: request.imageWidth || 800,
imageHeight: request.imageHeight || 300,
imageQuality: request.imageQuality || 90,
imageFormat: request.imageFormat || 'png',
backgroundColor: request.backgroundColor || '#ffffff',
textColor: request.textColor || '#000000'
})
return response.data
} catch (error) {
throw this.handleError(error)
}
}
private handleError(error: any): Error {
if (axios.isAxiosError(error)) {
const status = error.response?.status
const message = error.response?.data?.error || error.message
switch (status) {
case 401:
return new Error('Invalid API key. Please check your credentials.')
case 402:
return new Error('Insufficient credits. Please upgrade your plan.')
case 429:
return new Error('Rate limit exceeded. Please try again later.')
case 400:
return new Error(`Validation error: ${message}`)
default:
return new Error(`API Error: ${message}`)
}
}
return new Error('Network error occurred')
}
}
// Singleton instance
export const calligraphyApi = CalligraphyAPI.getInstance()React Components
Calligraphy Generator Component
typescript
Click to copy
// components/CalligraphyGenerator.tsx
'use client'
import { useState, useCallback } from 'react'
import { CalligraphyAPI, CalligraphyResult, CalligraphyUsage } from '@/lib/calligraphy-api'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { toast } from 'sonner'
import { Loader2, Download, Share2 } from 'lucide-react'
import { saveAs } from 'file-saver'
interface CalligraphyGeneratorProps {
className?: string
}
export default function CalligraphyGenerator({ className }: CalligraphyGeneratorProps) {
const [inputText, setInputText] = useState('नमस्ते दुनिया')
const [language, setLanguage] = useState('hindi')
const [fontStyle, setFontStyle] = useState('calligraphy')
const [count, setCount] = useState(3)
const [isLoading, setIsLoading] = useState(false)
const [results, setResults] = useState<CalligraphyResult[]>([])
const [usage, setUsage] = useState<CalligraphyUsage | null>(null)
const calligraphyApi = CalligraphyAPI.getInstance()
const languages = [
{ value: 'hindi', label: 'Hindi' },
{ value: 'marathi', label: 'Marathi' },
{ value: 'gujarati', label: 'Gujarati' },
{ value: 'english', label: 'English' }
]
const fontStyles = [
{ value: 'calligraphy', label: 'Calligraphy' },
{ value: 'decorative', label: 'Decorative' },
{ value: 'publication', label: 'Publication' }
]
const generateCalligraphy = useCallback(async () => {
if (!inputText.trim()) {
toast.error('Please enter some text')
return
}
setIsLoading(true)
try {
const response = await calligraphyApi.generateCalligraphy({
text: inputText.trim(),
language,
fontStyle,
count
})
if (response.success) {
setResults(response.data.results)
setUsage(response.data.usage)
toast.success(`Generated ${response.data.results.length} results!`)
} else {
toast.error('Failed to generate calligraphy')
}
} catch (error) {
toast.error(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}, [inputText, language, fontStyle, count, calligraphyApi])
const downloadSvg = async (result: CalligraphyResult) => {
try {
const response = await calligraphyApi.downloadSvg({
resultText: result.resultText,
fontName: result.fontFamily
})
if (response.success) {
const blob = new Blob([response.data.svgString], { type: 'image/svg+xml' })
saveAs(blob, `calligraphy-${Date.now()}.svg`)
toast.success('SVG downloaded!')
}
} catch (error) {
toast.error('Failed to download SVG')
}
}
const downloadImage = async (result: CalligraphyResult, format: 'png' | 'jpg' = 'png') => {
try {
const response = await calligraphyApi.downloadImage({
resultText: result.resultText,
fontName: result.fontFamily,
imageFormat: format
})
if (response.success) {
// Convert base64 to blob
const base64Data = response.data.imageBase64.split(',')[1]
const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: `image/${format}` })
saveAs(blob, `calligraphy-${Date.now()}.${format}`)
toast.success(`${format.toUpperCase()} downloaded!`)
}
} catch (error) {
toast.error(`Failed to download ${format.toUpperCase()}`)
}
}
const shareResult = async (result: CalligraphyResult) => {
if (navigator.share && result.cdnUrl) {
try {
await navigator.share({
title: 'Beautiful Calligraphy',
text: `Check out this calligraphy: ${result.resultText}`,
url: result.cdnUrl
})
} catch (error) {
// Fallback to clipboard
navigator.clipboard.writeText(result.cdnUrl)
toast.success('Image URL copied to clipboard!')
}
} else if (result.cdnUrl) {
navigator.clipboard.writeText(result.cdnUrl)
toast.success('Image URL copied to clipboard!')
}
}
return (
<div className={className}>
<Card>
<CardHeader>
<CardTitle>Calligraphy Generator</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Input Controls */}
<div className="grid gap-4">
<div>
<Label htmlFor="text-input">Text to Convert</Label>
<Textarea
id="text-input"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Enter text to convert to calligraphy"
className="min-h-[100px]"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label>Language</Label>
<Select value={language} onValueChange={setLanguage}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{languages.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Font Style</Label>
<Select value={fontStyle} onValueChange={setFontStyle}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{fontStyles.map((style) => (
<SelectItem key={style.value} value={style.value}>
{style.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="count">Count: {count}</Label>
<Input
id="count"
type="range"
min="1"
max="5"
value={count}
onChange={(e) => setCount(Number(e.target.value))}
className="mt-2"
/>
</div>
</div>
<div className="flex gap-4">
<Button
onClick={generateCalligraphy}
disabled={isLoading || !inputText.trim()}
className="flex-1"
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
'Generate Calligraphy'
)}
</Button>
<Button
variant="outline"
onClick={() => {
setResults([])
setUsage(null)
}}
disabled={results.length === 0}
>
Clear
</Button>
</div>
{usage && (
<div className="text-sm text-gray-600 text-center">
Credits remaining: {usage.remaining_credits}
</div>
)}
</div>
{/* Results Grid */}
{results.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-4">
Generated Results ({results.length})
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{results.map((result) => (
<Card key={result.id} className="overflow-hidden">
<div className="aspect-[4/3] bg-gray-100 flex items-center justify-center">
{result.cdnUrl ? (
<img
src={result.cdnUrl}
alt={result.resultText}
className="max-w-full max-h-full object-contain"
/>
) : (
<div className="text-gray-500">No image available</div>
)}
</div>
<CardContent className="p-4">
<p className="font-medium text-sm mb-1 truncate">
{result.resultText}
</p>
<p className="text-xs text-gray-500 mb-3">
{result.fontFamily} • {result.appliedVariants.length} variants
</p>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => downloadSvg(result)}
className="flex-1"
>
<Download className="mr-1 h-3 w-3" />
SVG
</Button>
<Button
size="sm"
variant="outline"
onClick={() => downloadImage(result, 'png')}
className="flex-1"
>
<Download className="mr-1 h-3 w-3" />
PNG
</Button>
<Button
size="sm"
variant="outline"
onClick={() => shareResult(result)}
>
<Share2 className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)}
</CardContent>
</Card>
</div>
)
}Main Page Implementation
typescript
Click to copy
// app/page.tsx
import CalligraphyGenerator from '@/components/CalligraphyGenerator'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Calligraphy Generator - Beautiful Hindi, Marathi, Gujarati Text',
description: 'Generate stunning calligraphy with our AI-powered tool. Support for Hindi, Marathi, Gujarati, and English.',
keywords: ['calligraphy', 'hindi', 'marathi', 'gujarati', 'text generator']
}
export default function HomePage() {
return (
<main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="container mx-auto px-4 py-8">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Beautiful Calligraphy Generator
</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Transform your text into stunning calligraphy with support for
Hindi, Marathi, Gujarati, and English scripts.
</p>
</div>
<CalligraphyGenerator className="max-w-6xl mx-auto" />
</div>
</main>
)
}Server-Side Features
API Route Proxy
typescript
Click to copy
// app/api/calligraphy/generate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { CalligraphyAPI } from '@/lib/calligraphy-api'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate input on server side
if (!body.text || typeof body.text !== 'string') {
return NextResponse.json(
{ error: 'Text is required and must be a string' },
{ status: 400 }
)
}
if (body.text.length > 200) {
return NextResponse.json(
{ error: 'Text must be less than 200 characters' },
{ status: 400 }
)
}
// Use server-side API key
const calligraphyApi = CalligraphyAPI.getInstance()
const result = await calligraphyApi.generateCalligraphy({
text: body.text,
language: body.language || 'hindi',
fontStyle: body.fontStyle || 'calligraphy',
count: Math.min(body.count || 3, 5) // Limit count to 5
})
return NextResponse.json(result)
} catch (error) {
console.error('API Route Error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
// GET method for API documentation
export async function GET() {
return NextResponse.json({
message: 'Calligraphy Generation API',
methods: ['POST'],
description: 'Generate beautiful calligraphy from text',
parameters: {
text: 'string (required, max 200 chars)',
language: 'string (optional: hindi, marathi, gujarati, english)',
fontStyle: 'string (optional: calligraphy, decorative, publication)',
count: 'number (optional: 1-5, default 3)'
}
})
}Server-Side Rendering with Data
typescript
Click to copy
// app/gallery/page.tsx - SSR Example
import { CalligraphyAPI, CalligraphyResult } from '@/lib/calligraphy-api'
import { Metadata } from 'next'
import GalleryClient from './GalleryClient'
// This page demonstrates SSR with calligraphy data
export const metadata: Metadata = {
title: 'Calligraphy Gallery - Beautiful Examples',
description: 'Explore beautiful calligraphy examples generated with our API'
}
// Sample data for demonstration
const sampleTexts = [
{ text: 'नमस्ते', language: 'hindi' },
{ text: 'धन्यवाद', language: 'marathi' },
{ text: 'આભાર', language: 'gujarati' },
{ text: 'Welcome', language: 'english' }
]
async function generateSampleGallery(): Promise<CalligraphyResult[]> {
const calligraphyApi = CalligraphyAPI.getInstance()
const results: CalligraphyResult[] = []
try {
// Generate sample calligraphy for gallery
for (const sample of sampleTexts) {
const response = await calligraphyApi.generateCalligraphy({
text: sample.text,
language: sample.language,
count: 1
})
if (response.success && response.data.results.length > 0) {
results.push(response.data.results[0])
}
}
} catch (error) {
console.error('Failed to generate gallery:', error)
}
return results
}
export default async function GalleryPage() {
// Generate gallery data at build time / request time
const galleryItems = await generateSampleGallery()
return (
<main className="min-h-screen bg-gray-50">
<div className="container mx-auto px-4 py-8">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Calligraphy Gallery
</h1>
<p className="text-xl text-gray-600">
Beautiful examples of AI-generated calligraphy
</p>
</div>
<GalleryClient initialItems={galleryItems} />
</div>
</main>
)
}
// app/gallery/GalleryClient.tsx
'use client'
import { CalligraphyResult } from '@/lib/calligraphy-api'
import { Card, CardContent } from '@/components/ui/card'
interface GalleryClientProps {
initialItems: CalligraphyResult[]
}
export default function GalleryClient({ initialItems }: GalleryClientProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{initialItems.map((item) => (
<Card key={item.id} className="overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-square bg-white flex items-center justify-center p-4">
{item.cdnUrl ? (
<img
src={item.cdnUrl}
alt={item.resultText}
className="max-w-full max-h-full object-contain"
/>
) : (
<div className="text-gray-500">Loading...</div>
)}
</div>
<CardContent className="p-4">
<h3 className="font-semibold text-lg mb-1">{item.resultText}</h3>
<p className="text-sm text-gray-600">{item.fontFamily}</p>
<p className="text-xs text-gray-500 mt-2">
{item.appliedVariants.length} variants applied
</p>
</CardContent>
</Card>
))}
</div>
)
}Custom Hooks for Data Fetching
typescript
Click to copy
// hooks/useCalligraphy.ts
import { useState, useCallback } from 'react'
import useSWR from 'swr'
import { CalligraphyAPI, CalligraphyResult, GenerateRequest } from '@/lib/calligraphy-api'
const calligraphyApi = CalligraphyAPI.getInstance()
// Hook for generating calligraphy
export function useCalligraphyGeneration() {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const generate = useCallback(async (request: GenerateRequest): Promise<CalligraphyResult[] | null> => {
setIsLoading(true)
setError(null)
try {
const response = await calligraphyApi.generateCalligraphy(request)
if (response.success) {
return response.data.results
} else {
setError('Failed to generate calligraphy')
return null
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
setError(errorMessage)
return null
} finally {
setIsLoading(false)
}
}, [])
return { generate, isLoading, error }
}
// Hook with SWR for caching
export function useCalligraphyWithCache(request: GenerateRequest | null) {
const { data, error, isLoading, mutate } = useSWR(
request ? ['calligraphy', request] : null,
async ([, req]) => {
const response = await calligraphyApi.generateCalligraphy(req)
return response.success ? response.data.results : []
},
{
revalidateOnFocus: false,
dedupingInterval: 60000, // Cache for 1 minute
}
)
return {
results: data || [],
error,
isLoading,
regenerate: mutate
}
}
// Hook for downloading
export function useCalligraphyDownload() {
const [isDownloading, setIsDownloading] = useState(false)
const downloadSvg = useCallback(async (resultText: string, fontName: string): Promise<string | null> => {
setIsDownloading(true)
try {
const response = await calligraphyApi.downloadSvg({ resultText, fontName })
if (response.success) {
return response.data.svgString
}
return null
} catch (error) {
console.error('SVG download failed:', error)
return null
} finally {
setIsDownloading(false)
}
}, [])
const downloadImage = useCallback(async (
resultText: string,
fontName: string,
options: {
format?: 'png' | 'jpg'
width?: number
height?: number
quality?: number
} = {}
): Promise<string | null> => {
setIsDownloading(true)
try {
const response = await calligraphyApi.downloadImage({
resultText,
fontName,
imageFormat: options.format || 'png',
imageWidth: options.width || 800,
imageHeight: options.height || 300,
imageQuality: options.quality || 90
})
if (response.success) {
return response.data.imageBase64
}
return null
} catch (error) {
console.error('Image download failed:', error)
return null
} finally {
setIsDownloading(false)
}
}, [])
return { downloadSvg, downloadImage, isDownloading }
}Deployment & Optimization
Vercel Deployment
bash
Click to copy
# vercel.json
{
"functions": {
"app/api/calligraphy/*/route.ts": {
"maxDuration": 30
}
},
"env": {
"CALLIGRAPHY_API_KEY": "@calligraphy-api-key"
}
}
# Deploy to Vercel
npm install -g vercel
vercel --prod
# Set environment variables
vercel env add CALLIGRAPHY_API_KEY production
vercel env add NEXT_PUBLIC_CALLIGRAPHY_API_BASE_URL productionPerformance Optimizations
javascript
Click to copy
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['api.calligraphymaker.com', 'cdn.calligraphymaker.com'],
formats: ['image/webp', 'image/avif'],
},
experimental: {
optimizeCss: true,
},
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
async headers() {
return [
{
source: '/api/calligraphy/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=60, stale-while-revalidate=300',
},
],
},
]
},
}
module.exports = nextConfig
// lib/image-optimization.ts
import Image from 'next/image'
export function OptimizedCalligraphyImage({
src,
alt,
width = 400,
height = 300,
priority = false
}: {
src: string
alt: string
width?: number
height?: number
priority?: boolean
}) {
return (
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
className="rounded-lg shadow-md"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
)
}What You'll Build
Full-Stack App
Client + Server-side rendering
API Route Proxy
Secure server-side API calls
SWR Integration
Data fetching with caching
Image Optimization
Next.js Image component
Requirements
Next.js 14+
React 18+
TypeScript 5+
API Key from Calligraphy API
Other Integrations