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

Published: (January 7, 2026 at 09:52 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Cover image for I Built a Free Business Card Generator with Next.js 14 – Here’s What I Learned

freecardapp

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 sign‑up 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 colours and fonts
  • ✅ PNG, PDF, and vCard export
  • ✅ Email‑signature generator
  • ✅ No sign‑up required
  • ✅ No watermarks, ever

Business model? Simple – Google AdSense. No premium tier, no upsells.

🛠️ Tech Stack

LayerTechnology
FrontendNext.js 14 (App Router)
LanguageTypeScript
StylingTailwind CSS + shadcn/ui
StateZustand
DatabaseMongoDB + Mongoose
AuthNextAuth.js v5
Exporthtml-to-image + jsPDF
QRqrcode.react
HostingVercel

Why This Stack?

  • Next.js 14 App Router – Server components by default = faster initial load. The new router is production‑ready.
  • Zustand over Redux – For a tool like this, Zustand’s simplicity wins. No boilerplate, just works:
// 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((set) => ({
  card: initialCard,
  setField: (field, value) =>
    set((state) => ({ card: { ...state.card, [field]: value } })),
  setTemplate: (templateId) =>
    set((state) => ({ card: { ...state.card, template: templateId } })),
}));
  • shadcn/ui – Not a component library, but a collection of 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 must:

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

A simplified example:

// 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 style={{ backgroundColor: colors?.background ?? '#fff' }}>
      <h1>{fullName || 'Your Name'}</h1>
      <h2>{title || 'Your Title'}</h2>
      {/* …more fields… */}
      {showQR && <QRCode value={generateVCard(card)} />}
    </div>
  );
}

📤 The Export Challenge

Exporting HTML to PNG/PDF sounds simple until you actually try it. Here’s what worked for me.

PNG Export

import { 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)

import 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" × 2" (88.9 mm × 50.8 mm)
  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:

export 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');
}

🐛 Challenges & Solutions

Fonts Not Rendering in Export

Problem: Custom fonts fell back to system fonts in PNG exports.
Solution: Ensure fonts are fully loaded before calling toPng:

await document.fonts.ready;
await new Promise((resolve) => setTimeout(resolve, 100)); // tiny delay
const dataUrl = await toPng(element);

QR Code Sizing

Problem: QR codes appeared blurry or were cut off at high resolutions.
Solution: Render the QR code at a larger size (pixelRatio = 3) and then scale it down in the PDF/PNG export. This preserves crisp edges while keeping the visual footprint small.

Color Picker Performance

Problem: Real‑time preview lagged with continuous color changes.

Solution: Debounced updates.

const 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 Cost
Vercel Hosting$0 (Hobby)
MongoDB Atlas$0 (Free tier)
Domain (.app)~ $1.25 (≈ $15/yr)
Total~ $1.25

That’s it—a fully functional SaaS‑like product for about $15/year.

🎓 Lessons Learned

  • Ship Fast, Iterate Later – Launched with 5 templates; now there are 25. The first version was “good enough”; users told me what they actually wanted.
  • “Free” is a Feature – In a market full of “freemium” tools with aggressive upsells, being genuinely free is a differentiator.
  • 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.
  • shadcn/ui is Amazing – Copy‑paste components mean you own the code. No fighting with library abstractions.
  • 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

Back to Blog

Related posts

Read more »