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-saver

2. 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_here

3. 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 production

Performance 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