<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: freecardapp</title>
    <description>The latest articles on DEV Community by freecardapp (@freecardapp).</description>
    <link>https://dev.to/freecardapp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F570351%2F6106741b-6903-4db9-9253-06a72b278c71.png</url>
      <title>DEV Community: freecardapp</title>
      <link>https://dev.to/freecardapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/freecardapp"/>
    <language>en</language>
    <item>
      <title>I Built a Free Business Card Generator with Next.js 14 - Here's What I Learned</title>
      <dc:creator>freecardapp</dc:creator>
      <pubDate>Wed, 07 Jan 2026 14:52:37 +0000</pubDate>
      <link>https://dev.to/freecardapp/i-built-a-free-business-card-generator-with-nextjs-14-heres-what-i-learned-3bca</link>
      <guid>https://dev.to/freecardapp/i-built-a-free-business-card-generator-with-nextjs-14-heres-what-i-learned-3bca</guid>
      <description>&lt;p&gt;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.&lt;br&gt;
🎯 The Problem&lt;br&gt;
I was frustrated with existing business card generators:&lt;/p&gt;

&lt;p&gt;❌ Most require signup just to see your design&lt;br&gt;
❌ "Free" tools add watermarks to exports&lt;br&gt;
❌ Premium features locked behind $10-20/month paywalls&lt;br&gt;
❌ Overly complicated interfaces for a simple task&lt;/p&gt;

&lt;p&gt;I wanted something that just works. Enter a name, pick a template, download. Done.&lt;br&gt;
💡 The Solution&lt;br&gt;
FreeCard.app - A genuinely free business card generator with:&lt;/p&gt;

&lt;p&gt;✅ 25+ professional templates&lt;br&gt;
✅ Automatic QR code generation (vCard format)&lt;br&gt;
✅ Custom colors and fonts&lt;br&gt;
✅ PNG, PDF, and vCard export&lt;br&gt;
✅ Email signature generator&lt;br&gt;
✅ No signup required&lt;br&gt;
✅ No watermarks, ever&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;typescript// store/useCardStore.ts
import { create } from 'zustand';

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

export const useCardStore = create&amp;lt;CardStore&amp;gt;((set) =&amp;gt; ({
  card: initialCard,
  setField: (field, value) =&amp;gt;
    set((state) =&amp;gt; ({ card: { ...state.card, [field]: value } })),
  setTemplate: (templateId) =&amp;gt;
    set((state) =&amp;gt; ({ card: { ...state.card, template: templateId } })),
}));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;shadcn/ui - Not a component library, but copy-paste components. Full control, great defaults, accessible out of the box.&lt;br&gt;
🎨 The Template System&lt;br&gt;
One of the trickiest parts was building a flexible template system. Each template needs to:&lt;/p&gt;

&lt;p&gt;Accept the same props (name, title, colors, etc.)&lt;br&gt;
Render differently based on design&lt;br&gt;
Be exportable as PNG/PDF&lt;/p&gt;

&lt;p&gt;Here's a simplified version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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 (
    &amp;lt;div 
      className="w-[350px] h-[200px] rounded-lg p-6 flex"
      style={{ backgroundColor: colors.background }}
    &amp;gt;
      &amp;lt;div className="flex-1"&amp;gt;
        &amp;lt;h2 
          className="text-xl font-bold"
          style={{ color: colors.text }}
        &amp;gt;
          {fullName || 'Your Name'}
        &amp;lt;/h2&amp;gt;
        &amp;lt;p style={{ color: colors.primary }}&amp;gt;{title || 'Your Title'}&amp;lt;/p&amp;gt;
        {/* ... more fields */}
      &amp;lt;/div&amp;gt;

      {showQR &amp;amp;&amp;amp; (
        &amp;lt;QRCodeSVG 
          value={generateVCard(card)} 
          size={80}
          bgColor="transparent"
          fgColor={colors.text}
        /&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📤 The Export Challenge&lt;br&gt;
Exporting HTML to PNG/PDF sounds simple until you actually try it. Here's what worked:&lt;br&gt;
PNG Export&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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');
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🐛 Challenges &amp;amp; Solutions&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;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 =&amp;gt; setTimeout(resolve, 100));
const dataUrl = await toPng(element);&lt;/li&gt;
&lt;li&gt;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&lt;/li&gt;
&lt;li&gt;Color Picker Performance
Problem: Real-time preview lagged with continuous color changes.
Solution: Debounced updates:
typescriptconst debouncedSetColor = useMemo(
() =&amp;gt; debounce((color: string) =&amp;gt; setColor('primary', color), 50),
[]
);
📊 Results (First Week)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🚀 Launched on Product Hunt&lt;br&gt;
📈 500+ unique visitors&lt;br&gt;
💾 200+ cards created&lt;br&gt;
🔗 Backlinks from BetaList, AlternativeTo&lt;br&gt;
💰 $0 revenue (AdSense pending approval)&lt;/p&gt;

&lt;p&gt;💸 Cost Breakdown&lt;br&gt;
ItemMonthly CostVercel Hosting$0 (Hobby plan)MongoDB Atlas$0 (Free tier)Domain (.app)~$1.25 ($15/year)Total~$1.25/month&lt;br&gt;
That's it. A fully functional SaaS-like product for about $15/year.&lt;br&gt;
🎓 Lessons Learned&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;"Free" is a Feature
In a market full of "freemium" tools with aggressive upsells, being genuinely free is a differentiator.&lt;/li&gt;
&lt;li&gt;Zustand &amp;gt; Redux for Small Projects
If your state fits in one file, you don't need Redux. Zustand's API is a joy to use.&lt;/li&gt;
&lt;li&gt;shadcn/ui is Amazing
Copy-paste components mean you own the code. No fighting with library abstractions.&lt;/li&gt;
&lt;li&gt;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&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;🙏 Try It Out&lt;br&gt;
If you need a business card, give it a try:&lt;br&gt;
👉 FreeCard.app&lt;br&gt;
No signup, no watermarks, no BS. Just free business cards.&lt;/p&gt;

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

&lt;p&gt;If you found this useful, consider:&lt;/p&gt;

&lt;p&gt;⭐ Upvoting on Product Hunt&lt;br&gt;
🐦 Following me on Twitter for more indie hacker content&lt;br&gt;
📧 Sharing with someone who needs business cards&lt;/p&gt;

&lt;p&gt;Tags&lt;/p&gt;

&lt;h1&gt;
  
  
  nextjs #typescript #react #webdev #opensource #indiehacker #buildinpublic #sideproject
&lt;/h1&gt;

</description>
      <category>learning</category>
      <category>nextjs</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
