DEV Community

Cover image for I Built a Free Business Card Generator with Next.js 14 - Here's What I Learned
freecardapp
freecardapp

Posted on

I Built a Free Business Card Generator with Next.js 14 - Here's What I Learned

Last week, I launched FreeCard.app - a completely free business card and QR code generator. In this post, I'll share the journey, tech decisions, challenges, and lessons learned.
๐ŸŽฏ The Problem
I was frustrated with existing business card generators:

โŒ Most require signup just to see your design
โŒ "Free" tools add watermarks to exports
โŒ Premium features locked behind $10-20/month paywalls
โŒ Overly complicated interfaces for a simple task

I wanted something that just works. Enter a name, pick a template, download. Done.
๐Ÿ’ก The Solution
FreeCard.app - A genuinely free business card generator with:

โœ… 25+ professional templates
โœ… Automatic QR code generation (vCard format)
โœ… Custom colors and fonts
โœ… PNG, PDF, and vCard export
โœ… Email signature generator
โœ… No signup required
โœ… No watermarks, ever

Business model? Simple - Google AdSense. No premium tier, no upsells.
๐Ÿ› ๏ธ Tech Stack
Frontend: Next.js 14 (App Router)
Language: TypeScript
Styling: Tailwind CSS + shadcn/ui
State: Zustand
Database: MongoDB + Mongoose
Auth: NextAuth.js v5
Export: html-to-image + jsPDF
QR: qrcode.react
Hosting: Vercel
Why This Stack?
Next.js 14 App Router - Server components by default = faster initial load. The new App Router is mature enough for production now.
Zustand over Redux - For a tool like this, Zustand's simplicity wins. No boilerplate, just works:

typescript// store/useCardStore.ts
import { create } from 'zustand';

interface CardStore {
  card: CardData;
  setField: (field: string, value: string) => void;
  setTemplate: (templateId: string) => void;
}

export const useCardStore = create<CardStore>((set) => ({
  card: initialCard,
  setField: (field, value) =>
    set((state) => ({ card: { ...state.card, [field]: value } })),
  setTemplate: (templateId) =>
    set((state) => ({ card: { ...state.card, template: templateId } })),
}));
Enter fullscreen mode Exit fullscreen mode

shadcn/ui - Not a component library, but copy-paste components. Full control, great defaults, accessible out of the box.
๐ŸŽจ The Template System
One of the trickiest parts was building a flexible template system. Each template needs to:

Accept the same props (name, title, colors, etc.)
Render differently based on design
Be exportable as PNG/PDF

Here's a simplified version:

typescript// templates/ModernDark.tsx
interface TemplateProps {
  card: CardData;
  showQR?: boolean;
}

export function ModernDark({ card, showQR = true }: TemplateProps) {
  const { fullName, title, email, phone, colors } = card;

  return (
    <div 
      className="w-[350px] h-[200px] rounded-lg p-6 flex"
      style={{ backgroundColor: colors.background }}
    >
      <div className="flex-1">
        <h2 
          className="text-xl font-bold"
          style={{ color: colors.text }}
        >
          {fullName || 'Your Name'}
        </h2>
        <p style={{ color: colors.primary }}>{title || 'Your Title'}</p>
        {/* ... more fields */}
      </div>

      {showQR && (
        <QRCodeSVG 
          value={generateVCard(card)} 
          size={80}
          bgColor="transparent"
          fgColor={colors.text}
        />
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ค The Export Challenge
Exporting HTML to PNG/PDF sounds simple until you actually try it. Here's what worked:
PNG Export

typescriptimport { toPng } from 'html-to-image';

export async function exportToPNG(elementId: string) {
  const element = document.getElementById(elementId);
  if (!element) throw new Error('Element not found');

  const dataUrl = await toPng(element, {
    quality: 1,
    pixelRatio: 3, // High resolution
    backgroundColor: '#ffffff',
  });

  // Trigger download
  const link = document.createElement('a');
  link.download = 'business-card.png';
  link.href = dataUrl;
  link.click();
}
PDF Export (Standard Business Card Size)
typescriptimport jsPDF from 'jspdf';
import { toPng } from 'html-to-image';

export async function exportToPDF(elementId: string) {
  const element = document.getElementById(elementId);
  const dataUrl = await toPng(element, { pixelRatio: 3 });

  // Standard business card: 3.5" x 2" (88.9mm x 50.8mm)
  const pdf = new jsPDF({
    orientation: 'landscape',
    unit: 'mm',
    format: [88.9, 50.8],
  });

  pdf.addImage(dataUrl, 'PNG', 0, 0, 88.9, 50.8);
  pdf.save('business-card.pdf');
}
vCard Generation
QR codes encode vCard data so people can scan and save contacts:
typescriptexport function generateVCard(card: CardData): string {
  return [
    'BEGIN:VCARD',
    'VERSION:3.0',
    `FN:${card.fullName}`,
    `ORG:${card.company || ''}`,
    `TITLE:${card.title || ''}`,
    `EMAIL:${card.email || ''}`,
    `TEL:${card.phone || ''}`,
    `URL:${card.website || ''}`,
    'END:VCARD',
  ].join('\n');
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ› Challenges & Solutions

  1. Fonts Not Rendering in Export Problem: Custom fonts appeared as system fonts in PNG exports. Solution: Ensure fonts are fully loaded before export: typescriptawait document.fonts.ready; await new Promise(resolve => setTimeout(resolve, 100)); const dataUrl = await toPng(element);
  2. QR Code Sizing Problem: QR codes were either too big (overwhelming) or too small (unscannable). Solution: Fixed size relative to card, minimum 60px for reliable scanning: typescript
  3. Color Picker Performance Problem: Real-time preview lagged with continuous color changes. Solution: Debounced updates: typescriptconst debouncedSetColor = useMemo( () => debounce((color: string) => setColor('primary', color), 50), [] ); ๐Ÿ“Š Results (First Week)

๐Ÿš€ Launched on Product Hunt
๐Ÿ“ˆ 500+ unique visitors
๐Ÿ’พ 200+ cards created
๐Ÿ”— Backlinks from BetaList, AlternativeTo
๐Ÿ’ฐ $0 revenue (AdSense pending approval)

๐Ÿ’ธ Cost Breakdown
ItemMonthly CostVercel Hosting$0 (Hobby plan)MongoDB Atlas$0 (Free tier)Domain (.app)~$1.25 ($15/year)Total~$1.25/month
That's it. A fully functional SaaS-like product for about $15/year.
๐ŸŽ“ Lessons Learned

  1. Ship Fast, Iterate Later I launched with 5 templates. Now there are 25. The first version was "good enough" - users told me what they actually wanted.
  2. "Free" is a Feature In a market full of "freemium" tools with aggressive upsells, being genuinely free is a differentiator.
  3. Zustand > Redux for Small Projects If your state fits in one file, you don't need Redux. Zustand's API is a joy to use.
  4. shadcn/ui is Amazing Copy-paste components mean you own the code. No fighting with library abstractions.
  5. The App Router is Ready After months of "should I use Pages or App Router?" - just use App Router. It's stable and the DX is great. ๐Ÿ”ฎ What's Next

More templates (targeting 50+)
NFC business card support
Public shareable links (freecard.app/c/username)
LinkedIn profile import
Template marketplace (user-submitted designs)

๐Ÿ™ Try It Out
If you need a business card, give it a try:
๐Ÿ‘‰ FreeCard.app
No signup, no watermarks, no BS. Just free business cards.

What do you think? I'd love to hear your feedback in the comments. What features would make this more useful for you?

If you found this useful, consider:

โญ Upvoting on Product Hunt
๐Ÿฆ Following me on Twitter for more indie hacker content
๐Ÿ“ง Sharing with someone who needs business cards

Tags

nextjs #typescript #react #webdev #opensource #indiehacker #buildinpublic #sideproject

Top comments (0)