我用 Next.js 14 构建了免费名片生成器——我的收获

发布: (2026年1月7日 GMT+8 22:52)
8 min read
原文: 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

上周,我推出了 FreeCard.app —— 一个完全免费的名片和二维码生成器。在本文中,我将分享整个过程、技术决策、遇到的挑战以及学到的经验。

🎯 问题

我对现有的名片生成器感到沮丧:

  • ❌ 大多数工具必须注册才能预览设计
  • ❌ “免费”工具在导出文件上添加水印
  • ❌ 高级功能被锁在每月 10‑20 美元的付费墙后面
  • ❌ 为了一个简单的任务而设计的界面过于复杂

我只想要一个直接可用的工具:输入姓名,选择模板,下载。完成。

💡 解决方案

FreeCard.app – 一个真正免费的名片生成器,具备:

  • ✅ 25+ 专业模板
  • ✅ 自动二维码生成(vCard 格式)
  • ✅ 自定义颜色和字体
  • ✅ PNG、PDF 和 vCard 导出
  • ✅ 邮件签名生成器
  • ✅ 无需注册
  • ✅ 永不添加水印

商业模式? 简单——Google AdSense。没有高级版,也没有增销。

🛠️ 技术栈

技术
前端Next.js 14 (App Router)
语言TypeScript
样式Tailwind CSS + shadcn/ui
状态Zustand
数据库MongoDB + Mongoose
认证NextAuth.js v5
导出html-to-image + jsPDF
二维码qrcode.react
托管Vercel

为什么选择这套技术栈?

  • Next.js 14 App Router – 默认使用服务器组件 = 更快的首次加载。新路由已可用于生产。
  • Zustand 优于 Redux – 对于此类工具,Zustand 的简洁性占优势。无需模板代码,直接可用:
// 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 – 不是组件库,而是一套可复制粘贴的组件集合。完全可控,默认配置优秀,开箱即用且可访问。

🎨 模板系统

构建一个灵活的模板系统是最棘手的部分之一。每个模板必须:

  1. 接受相同的属性(name、title、colours 等)
  2. 根据设计呈现不同的效果
  3. 能导出为 PNG/PDF

一个简化的示例:

// 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>
  );
}

📤 导出挑战

将 HTML 导出为 PNG/PDF 听起来很简单,直到你真的去尝试。以下是我成功使用的方法。

PNG 导出

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 导出(标准名片尺寸)

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 生成

二维码可以编码 vCard 数据,方便人们扫描并保存联系人信息:

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

🐛 挑战与解决方案

导出时字体未渲染

问题: 自定义字体在 PNG 导出时回退为系统字体。
解决方案: 在调用 toPng 之前确保字体已完全加载:

await document.fonts.ready;
await new Promise((resolve) => setTimeout(resolve, 100)); // 短暂延迟
const dataUrl = await toPng(element);

二维码尺寸

问题: 二维码在高分辨率下出现模糊或被裁剪。
解决方案: 将二维码以更大的尺寸渲染(pixelRatio = 3),然后在 PDF/PNG 导出时缩小。这可以在保持视觉占用面积小的同时,保留清晰的边缘。

颜色选择器性能

问题: 实时预览在持续更改颜色时出现卡顿。

解决方案: 使用防抖更新。

const debouncedSetColor = useMemo(
  () => debounce((color: string) => setColor('primary', color), 50),
  []
);

📊 首周结果

  • 🚀 在 Product Hunt 上发布
  • 📈 500+ 独立访客
  • 💾 200+ 张卡片已创建
  • 🔗 来自 BetaListAlternativeTo 的反向链接
  • 💰 $0 收入(AdSense 待批准)

💸 成本明细

项目月费用
Vercel Hosting$0 (Hobby)
MongoDB Atlas$0 (Free tier)
Domain (.app)~ $1.25 (≈ $15/yr)
Total~ $1.25

就是这样——一个功能完整的 SaaS‑like 产品,成本约为 $15/年

🎓 Lessons Learned

  • Ship Fast, Iterate Later – 上线时只有 5 个模板;现在已经有 25 个。首个版本“够用”,用户会告诉我他们真正想要的功能。
  • “Free” is a Feature – 在充斥着“免费增值”工具并伴随激进推销的市场中,真正免费本身就是一种差异化优势。
  • Zustand > Redux for Small Projects – 如果你的状态可以放在一个文件里,就不需要 Redux。Zustand 的 API 使用起来非常愉快。
  • shadcn/ui is Amazing – 复制‑粘贴组件意味着你拥有代码本身。无需与库的抽象层争斗。
  • The App Router is Ready – 经过数月的“该用 Pages 还是 App Router?”的犹豫后——直接使用 App Router。它已经稳定,开发体验极佳。

🔮 接下来

  • 更多模板(目标 50+
  • NFC 名片支持
  • 可公开分享的链接(freecard.app/c/username
  • LinkedIn 个人资料导入
  • 模板市场(用户提交的设计)

🙏 试一试

如果你需要名片,试试看:

👉 FreeCard.app

无需注册,无水印,毫无废话。只有免费名片。

你怎么看? 我很想在评论中听到你的反馈。哪些功能会让它对你更有用?

如果你觉得这有用,请考虑:

  • ⭐ 在 Product Hunt 上点赞
  • 🐦 在 Twitter 上关注我,获取更多独立黑客内容
  • 📧 与需要名片的人分享

标签

nextjs typescript react webdev opensource indiehacker buildinpublic sideproject

Back to Blog

相关文章

阅读更多 »

React 组件中的 TypeScript 泛型

介绍:泛型并不是在 React 组件中每天都会使用的东西,但在某些情况下,它们可以让你编写既灵活又类型安全的组件。